Compare commits
87 Commits
v1.4.0
...
next-relea
Author | SHA1 | Date |
---|---|---|
|
cc9b45c6a1 | |
|
3be2d73c60 | |
|
34ad357f47 | |
|
2d9409d147 | |
|
35e8de4cfa | |
|
e2beba299b | |
|
3c65582d8e | |
|
a9f5c36d49 | |
|
f6b27978e9 | |
|
0ef0a1dd39 | |
|
7fa1ac7fe6 | |
|
18486012c6 | |
|
d48d01f051 | |
|
1701fbbeb1 | |
|
a7c281d740 | |
|
01ebac7e74 | |
|
104559d03d | |
|
173e5ebba5 | |
|
0dc01da2b0 | |
|
945ce80840 | |
|
16ebe8d543 | |
|
0c29e522ed | |
|
c51c368d83 | |
|
0446d267ef | |
|
100fb3487d | |
|
e501bef249 | |
|
71ac97410a | |
|
5696ac57bd | |
|
f759581157 | |
|
08b5af7511 | |
|
4581af5e9f | |
|
7f7a51d19b | |
|
739d05b095 | |
|
212b609e26 | |
|
bd6fc1576b | |
|
53df6183bd | |
|
ffeb009497 | |
|
3786fcc2c2 | |
|
e53c12ba74 | |
|
c5a5e33256 | |
|
924547c316 | |
|
5f8dd8b4a0 | |
|
90ae3cf9cb | |
|
6b9bf53b4a | |
|
4fd2950027 | |
|
2ce2a46c95 | |
|
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 |
|
@ -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
|
||||
|
||||
|
|
|
@ -7,52 +7,7 @@ stages:
|
|||
include:
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
|
||||
##############
|
||||
# TEST STAGE #
|
||||
##############
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: docker/compose:latest
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ''
|
||||
services:
|
||||
- docker:dind
|
||||
script:
|
||||
- docker-compose -f docker-compose.ci.tools.yml -p matcher-tools --env-file ci/.env.ci up -d
|
||||
- sh ci/wait-up.sh
|
||||
- docker-compose -f docker-compose.ci.service.yml -p matcher-service --env-file ci/.env.ci up -d
|
||||
- docker exec -t v3-matcher-api sh -c "npm run test:integration:ci"
|
||||
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
||||
when: always
|
||||
|
||||
###############
|
||||
# BUILD STAGE #
|
||||
###############
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:20.10.22
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ''
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
|
||||
script:
|
||||
- export VERSION=$(docker run --rm -v "$PWD":/usr/src/app:ro -w /usr/src/app node:slim node -p "require('./package.json').version")
|
||||
- docker pull $CI_REGISTRY_IMAGE:latest || true
|
||||
- >
|
||||
docker build
|
||||
--pull
|
||||
--cache-from $CI_REGISTRY_IMAGE:latest
|
||||
--tag $CI_REGISTRY_IMAGE:$VERSION
|
||||
--tag $CI_REGISTRY_IMAGE:latest
|
||||
.
|
||||
- docker push $CI_REGISTRY_IMAGE:$VERSION
|
||||
- docker push $CI_REGISTRY_IMAGE:latest
|
||||
only:
|
||||
- main
|
||||
- project: mobicoop/v3/gitlab-templates
|
||||
file:
|
||||
- /ci/release.build-job.yml
|
||||
- /ci/service.test-job.yml
|
||||
|
|
|
@ -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_
|
||||
- [ ] ...
|
|
@ -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_
|
||||
- [ ] ...
|
|
@ -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 ./
|
||||
|
|
12
README.md
12
README.md
|
@ -156,13 +156,18 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
- **strict** (boolean, optional): if set to true, allow matching only with similar frequency ads (_default : false_)
|
||||
- **fromDate**: start date for recurrent ad, carpool date for punctual ad
|
||||
- **toDate**: end date for recurrent ad, same as fromDate for punctual ad
|
||||
- **schedule**: an array of schedule items, a schedule item containing :
|
||||
- **schedule**: an optional array of schedule items, a schedule item containing :
|
||||
|
||||
- the week day as a number, from 0 (sunday) to 6 (saturday) if the ad is recurrent (default to fromDate day for punctual search)
|
||||
- the departure time (as HH:MM)
|
||||
- the margin around the departure time in seconds (optional) (_default : 900_)
|
||||
|
||||
_If the schedule is not set, the driver departure time is guessed to be the ideal departure time to reach the passenger, and the passenger departure time is guessed to be the ideal pick up time for the driver_
|
||||
|
||||
- **seatsProposed** (integer, optional): number of seats proposed as driver (_default : 3_)
|
||||
- **seatsRequested** (integer, optional): number of seats requested as passenger (_default : 1_)
|
||||
- **waypoints**: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
|
||||
- **excludedAdId** (optional): the id of an ad to be excluded from the results (useful to avoid self-matchings)
|
||||
- **algorithmType** (optional): the type of algorithm to use (as of 2023-09-28, only the default `PASSENGER_ORIENTED` is accepted)
|
||||
- **remoteness** (integer, optional): an integer to indicate the maximum flying distance (in metres) between the driver route and the passenger pick-up / drop-off points (_default : 15000_)
|
||||
- **useProportion** (boolean, optional): a boolean to indicate if the matching algorithm will compare the distance of the passenger route against the distance of the driver route (_default : 1_). Works in combination with **proportion** parameter
|
||||
|
@ -181,7 +186,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
|
||||
|
@ -215,10 +219,6 @@ If the matching is successful, you will get a result, containing :
|
|||
Matching is a time-consuming process, so the results of a matching request are stored in cache before being paginated and returned to the requester.
|
||||
An id is attributed to the overall results of a request : on further requests (for example to query for different pages of results), the requester can provide this id and get in return the cached data, avoiding another longer process of computing the results from scratch. Obviously, new computing must be done periodically to get fresh new results !
|
||||
|
||||
There's also a basic cache to store the results of the _same_ request sent multiple times successively.
|
||||
|
||||
Cache TTLs are customizable in the `.env` file.
|
||||
|
||||
## Tests / ESLint / Prettier
|
||||
|
||||
Tests are run outside the container for ease of use (switching between different environments inside containers using prisma is complicated and error prone).
|
||||
|
|
11
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
|
||||
|
|
|
@ -11,10 +11,11 @@ services:
|
|||
- .:/usr/src/app
|
||||
env_file:
|
||||
- .env
|
||||
command: npm run start:dev
|
||||
command: npm run start:debug
|
||||
ports:
|
||||
- ${SERVICE_PORT:-5005}:${SERVICE_PORT:-5005}
|
||||
- ${HEALTH_SERVICE_PORT:-6005}:${HEALTH_SERVICE_PORT:-6005}
|
||||
- 9225:9229
|
||||
networks:
|
||||
v3-network:
|
||||
aliases:
|
||||
|
|
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mobicoop/matcher",
|
||||
"version": "1.4.0",
|
||||
"version": "1.8.0",
|
||||
"description": "Mobicoop V3 Matcher",
|
||||
"author": "sbriat",
|
||||
"private": true,
|
||||
|
@ -11,7 +11,7 @@
|
|||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
||||
|
@ -22,71 +22,71 @@
|
|||
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
|
||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:watch": "jest --testPathPattern 'tests/unit/' --watch",
|
||||
"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.5.0",
|
||||
"@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,26 @@ 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';
|
||||
export const MATCHER_AD_UPDATED_ROUTING_KEY = 'matcher-ad.updated';
|
||||
export const MATCHER_AD_UPDATE_FAILED_ROUTING_KEY = 'matcher-ad.update-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_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_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_DELETED_QUEUE = 'matcher.ad.deleted';
|
||||
|
||||
// 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({
|
||||
|
|
|
@ -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,
|
||||
}));
|
|
@ -0,0 +1,24 @@
|
|||
import { ArgumentsHost, Catch, Logger } from '@nestjs/common';
|
||||
import { BaseRpcExceptionFilter } from '@nestjs/microservices';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Catch()
|
||||
export class LogCauseExceptionFilter extends BaseRpcExceptionFilter {
|
||||
private static readonly causeLogger = new Logger('RpcExceptionsHandler');
|
||||
|
||||
catch(exception: any, host: ArgumentsHost): Observable<any> {
|
||||
const response = super.catch(exception, host);
|
||||
const cause = exception.cause;
|
||||
if (cause) {
|
||||
if (this.isError(cause)) {
|
||||
LogCauseExceptionFilter.causeLogger.error(
|
||||
'Caused by: ' + cause.message,
|
||||
cause.stack,
|
||||
);
|
||||
} else {
|
||||
LogCauseExceptionFilter.causeLogger.error('Caused by: ' + cause);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
import {
|
||||
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,4 @@ export const OUTPUT_DATETIME_TRANSFORMER = Symbol(
|
|||
export const AD_CONFIGURATION_REPOSITORY = Symbol(
|
||||
'AD_CONFIGURATION_REPOSITORY',
|
||||
);
|
||||
export const GEOGRAPHY_PACKAGE = Symbol('GEOGRAPHY_PACKAGE');
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import { ExtendedMapper } from '@mobicoop/ddd-library';
|
||||
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AdEntity } from './core/domain/ad.entity';
|
||||
import {
|
||||
AdWriteModel,
|
||||
AdReadModel,
|
||||
ScheduleItemModel,
|
||||
AdWriteExtraModel,
|
||||
} from './infrastructure/ad.repository';
|
||||
import { v4 } from 'uuid';
|
||||
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
|
||||
import { AdEntity } from './core/domain/ad.entity';
|
||||
import {
|
||||
ScheduleItem,
|
||||
ScheduleItemProps,
|
||||
} from './core/domain/value-objects/schedule-item.value-object';
|
||||
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
|
||||
import { ExtendedMapper } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AdReadModel,
|
||||
AdWriteExtraModel,
|
||||
AdWriteModel,
|
||||
ScheduleItemModel,
|
||||
ScheduleWriteModel,
|
||||
} from './infrastructure/ad.repository';
|
||||
|
||||
/**
|
||||
* Mapper constructs objects that are used in different layers:
|
||||
|
@ -38,9 +39,8 @@ export class AdMapper
|
|||
private readonly directionEncoder: DirectionEncoderPort,
|
||||
) {}
|
||||
|
||||
toPersistence = (entity: AdEntity): AdWriteModel => {
|
||||
toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
|
||||
const copy = entity.getProps();
|
||||
const now = new Date();
|
||||
const record: AdWriteModel = {
|
||||
uuid: copy.id,
|
||||
driver: copy.driver,
|
||||
|
@ -48,22 +48,7 @@ export class AdMapper
|
|||
frequency: copy.frequency,
|
||||
fromDate: new Date(copy.fromDate),
|
||||
toDate: new Date(copy.toDate),
|
||||
schedule: {
|
||||
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||
uuid: v4(),
|
||||
day: scheduleItem.day,
|
||||
time: new Date(
|
||||
1970,
|
||||
0,
|
||||
1,
|
||||
parseInt(scheduleItem.time.split(':')[0]),
|
||||
parseInt(scheduleItem.time.split(':')[1]),
|
||||
),
|
||||
margin: scheduleItem.margin,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
},
|
||||
schedule: this.toScheduleItemWriteModel(copy.schedule, update),
|
||||
seatsProposed: copy.seatsProposed,
|
||||
seatsRequested: copy.seatsRequested,
|
||||
strict: copy.strict,
|
||||
|
@ -73,12 +58,39 @@ export class AdMapper
|
|||
passengerDistance: copy.passengerDistance,
|
||||
fwdAzimuth: copy.fwdAzimuth,
|
||||
backAzimuth: copy.backAzimuth,
|
||||
createdAt: copy.createdAt,
|
||||
updatedAt: copy.updatedAt,
|
||||
};
|
||||
return record;
|
||||
};
|
||||
|
||||
toScheduleItemWriteModel = (
|
||||
schedule: ScheduleItemProps[],
|
||||
update?: boolean,
|
||||
): ScheduleWriteModel => {
|
||||
const now = new Date();
|
||||
const record: ScheduleWriteModel = {
|
||||
create: schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||
uuid: v4(),
|
||||
day: scheduleItem.day,
|
||||
time: new Date(
|
||||
1970,
|
||||
0,
|
||||
1,
|
||||
parseInt(scheduleItem.time.split(':')[0]),
|
||||
parseInt(scheduleItem.time.split(':')[1]),
|
||||
),
|
||||
margin: scheduleItem.margin,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
};
|
||||
if (update) {
|
||||
record.deleteMany = {
|
||||
createdAt: { lt: now },
|
||||
};
|
||||
}
|
||||
return record;
|
||||
};
|
||||
|
||||
toDomain = (record: AdReadModel): AdEntity =>
|
||||
new AdEntity({
|
||||
id: record.uuid,
|
||||
|
@ -97,7 +109,7 @@ export class AdMapper
|
|||
frequency: record.frequency,
|
||||
fromDate: record.fromDate.toISOString().split('T')[0],
|
||||
toDate: record.toDate.toISOString().split('T')[0],
|
||||
schedule: record.schedule.map(
|
||||
schedule: record.schedule?.map(
|
||||
(scheduleItem: ScheduleItemModel) =>
|
||||
new ScheduleItem({
|
||||
day: scheduleItem.day,
|
||||
|
@ -111,12 +123,14 @@ export class AdMapper
|
|||
margin: scheduleItem.margin,
|
||||
}),
|
||||
),
|
||||
waypoints: this.directionEncoder
|
||||
.decode(record.waypoints)
|
||||
.map((coordinates, index) => ({
|
||||
position: index,
|
||||
...coordinates,
|
||||
})),
|
||||
waypoints: record.waypoints
|
||||
? this.directionEncoder
|
||||
.decode(record.waypoints)
|
||||
.map((coordinates, index) => ({
|
||||
position: index,
|
||||
...coordinates,
|
||||
}))
|
||||
: [],
|
||||
fwdAzimuth: record.fwdAzimuth,
|
||||
backAzimuth: record.backAzimuth,
|
||||
points: [],
|
||||
|
|
|
@ -1,51 +1,73 @@
|
|||
import { Module, Provider } from '@nestjs/common';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
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,
|
||||
} from './ad.di-tokens';
|
||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { AdMapper } from './ad.mapper';
|
||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||
import { 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';
|
||||
import { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||
import { TimeConverter } from './infrastructure/time-converter';
|
||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||
import { 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';
|
||||
import { MatchingMapper } from './matching.mapper';
|
||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { redisStore } from 'cache-manager-ioredis-yet';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||
import {
|
||||
RedisClientOptions,
|
||||
RedisModule,
|
||||
RedisModuleOptions,
|
||||
} from '@songkeys/nestjs-redis';
|
||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||
import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants';
|
||||
import { redisStore } from 'cache-manager-ioredis-yet';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
AD_CONFIGURATION_REPOSITORY,
|
||||
AD_DIRECTION_ENCODER,
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
GEOGRAPHY_PACKAGE,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
MATCHING_REPOSITORY,
|
||||
OUTPUT_DATETIME_TRANSFORMER,
|
||||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
} from './ad.di-tokens';
|
||||
import { AdMapper } from './ad.mapper';
|
||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
|
||||
import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
|
||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||
import { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
import { Georouter } from './infrastructure/georouter';
|
||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||
import { MatchingRepository } from './infrastructure/matching.repository';
|
||||
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
||||
import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { TimeConverter } from './infrastructure/time-converter';
|
||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||
import { AdDeletedMessageHandler } from './interface/message-handlers/ad-deleted.message-handler';
|
||||
import { AdUpdatedMessageHandler } from './interface/message-handlers/ad-updated.message-handler';
|
||||
import { MatchMapper } from './match.mapper';
|
||||
import { MatchingMapper } from './matching.mapper';
|
||||
|
||||
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')}`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
]),
|
||||
CacheModule.registerAsync<RedisClientOptions>({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
|
@ -78,9 +100,21 @@ const imports = [
|
|||
|
||||
const grpcControllers = [MatchGrpcController];
|
||||
|
||||
const messageHandlers = [AdCreatedMessageHandler];
|
||||
const messageHandlers = [
|
||||
AdCreatedMessageHandler,
|
||||
AdUpdatedMessageHandler,
|
||||
AdDeletedMessageHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [CreateAdService];
|
||||
const eventHandlers: Provider[] = [
|
||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [
|
||||
CreateAdService,
|
||||
UpdateAdService,
|
||||
DeleteAdService,
|
||||
];
|
||||
|
||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||
|
||||
|
@ -117,15 +151,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 +176,7 @@ const adapters: Provider[] = [
|
|||
controllers: [...grpcControllers],
|
||||
providers: [
|
||||
...messageHandlers,
|
||||
...eventHandlers,
|
||||
...commandHandlers,
|
||||
...queryHandlers,
|
||||
...mappers,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
import { ScheduleItem } from '../../types/schedule-item.type';
|
||||
import { Frequency, UserAd } from '@modules/ad/core/domain/ad.types';
|
||||
import { Address } from '../../types/address.type';
|
||||
import { ScheduleItem } from '../../types/schedule-item.type';
|
||||
|
||||
export class CreateAdCommand extends Command {
|
||||
export class CreateAdCommand extends Command implements UserAd {
|
||||
readonly id: string;
|
||||
readonly driver: boolean;
|
||||
readonly passenger: boolean;
|
||||
|
|
|
@ -1,127 +1,63 @@
|
|||
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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { AggregateID, ConflictException } 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,
|
||||
PathCreator,
|
||||
PathType,
|
||||
TypedRoute,
|
||||
} from '@modules/ad/core/domain/path-creator.service';
|
||||
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';
|
||||
AggregateID,
|
||||
ConflictException,
|
||||
MessagePublisherPort,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||
import { AdFactory } from '@modules/ad/core/domain/ad.factory';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { GeorouterService } from '../../../domain/georouter.service';
|
||||
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { CreateAdCommand } from './create-ad.command';
|
||||
|
||||
@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: GeorouterService,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||
const roles: Role[] = [];
|
||||
if (command.driver) roles.push(Role.DRIVER);
|
||||
if (command.passenger) roles.push(Role.PASSENGER);
|
||||
|
||||
const pathCreator: PathCreator = new PathCreator(
|
||||
roles,
|
||||
command.waypoints.map(
|
||||
(waypoint: Waypoint) =>
|
||||
new PointValueObject({
|
||||
lon: waypoint.lon,
|
||||
lat: waypoint.lat,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
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');
|
||||
}
|
||||
const adFactory = new AdFactory(this.routeProvider);
|
||||
const ad = await adFactory.create(command);
|
||||
|
||||
let driverDistance: number | undefined;
|
||||
let driverDuration: number | undefined;
|
||||
let passengerDistance: number | undefined;
|
||||
let passengerDuration: number | undefined;
|
||||
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;
|
||||
try {
|
||||
//TODO it should not be this service's concern that Prisma does not support postgis types
|
||||
await this.repository.insertExtra(ad, 'ad');
|
||||
return ad.id;
|
||||
} catch (error: any) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw new AdAlreadyExistsException(error);
|
||||
}
|
||||
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,7 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class DeleteAdCommand extends Command {
|
||||
constructor(props: CommandProps<DeleteAdCommand>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { DeleteAdCommand } from './delete-ad.command';
|
||||
|
||||
@CommandHandler(DeleteAdCommand)
|
||||
export class DeleteAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY) private readonly adRepository: AdRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteAdCommand): Promise<boolean> {
|
||||
const ad = await this.adRepository.findOneById(command.id);
|
||||
ad.delete();
|
||||
return this.adRepository.delete(ad);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { CreateAdCommand } from '../create-ad/create-ad.command';
|
||||
|
||||
export class UpdateAdCommand extends CreateAdCommand {}
|
|
@ -0,0 +1,48 @@
|
|||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdFactory } from '@modules/ad/core/domain/ad.factory';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { MATCHER_AD_UPDATE_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { GeorouterService } from '../../../domain/georouter.service';
|
||||
import { MatcherAdUpdateFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { UpdateAdCommand } from './update-ad.command';
|
||||
|
||||
@CommandHandler(UpdateAdCommand)
|
||||
export class UpdateAdService 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: GeorouterService,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateAdCommand): Promise<void> {
|
||||
try {
|
||||
const adFactory = new AdFactory(this.routeProvider);
|
||||
const ad = await adFactory.create(command);
|
||||
return this.repository.update(ad.id, ad);
|
||||
} catch (error: any) {
|
||||
const integrationEvent = new MatcherAdUpdateFailedIntegrationEvent({
|
||||
id: command.id,
|
||||
metadata: {
|
||||
correlationId: command.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
cause: error.message,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
MATCHER_AD_UPDATE_FAILED_ROUTING_KEY,
|
||||
JSON.stringify(integrationEvent),
|
||||
);
|
||||
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,15 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class MatcherAdFailureIntegrationEvent extends IntegrationEvent {
|
||||
readonly cause?: string;
|
||||
|
||||
constructor(
|
||||
props: IntegrationEventProps<MatcherAdCreationFailedIntegrationEvent>,
|
||||
) {
|
||||
super(props);
|
||||
this.cause = props.cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class MatcherAdCreationFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {}
|
||||
export class MatcherAdUpdateFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {}
|
|
@ -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 '../../../../domain/georouter.service';
|
||||
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,17 @@ 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) => carpoolPathItem,
|
||||
),
|
||||
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
|
||||
|
@ -171,7 +165,7 @@ export class MatchQueryHandler implements IQueryHandler {
|
|||
frequency: query.frequency,
|
||||
fromDate: query.fromDate,
|
||||
toDate: query.toDate,
|
||||
schedule: query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
schedule: query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day as number,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin as number,
|
||||
|
|
|
@ -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 { GeorouterService } from '../../../domain/georouter.service';
|
||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.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;
|
||||
|
@ -21,11 +21,12 @@ export class MatchQuery extends QueryBase {
|
|||
readonly frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
schedule: ScheduleItem[];
|
||||
schedule?: ScheduleItem[];
|
||||
seatsProposed?: number;
|
||||
seatsRequested?: number;
|
||||
strict?: boolean;
|
||||
readonly waypoints: Waypoint[];
|
||||
excludedAdId?: string;
|
||||
algorithmType?: AlgorithmType;
|
||||
remoteness?: number;
|
||||
useProportion?: boolean;
|
||||
|
@ -40,9 +41,10 @@ export class MatchQuery extends QueryBase {
|
|||
passengerRoute?: Route;
|
||||
backAzimuth?: number;
|
||||
private readonly originWaypoint: Waypoint;
|
||||
routeProvider: RouteProviderPort;
|
||||
routeProvider: GeorouterService;
|
||||
|
||||
constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) {
|
||||
// TODO: remove MatchRequestDto depency (here core domain depends on interface /!\)
|
||||
constructor(props: MatchRequestDto, routeProvider: GeorouterService) {
|
||||
super();
|
||||
this.id = props.id;
|
||||
this.driver = props.driver;
|
||||
|
@ -55,6 +57,7 @@ export class MatchQuery extends QueryBase {
|
|||
this.seatsRequested = props.seatsRequested;
|
||||
this.strict = props.strict;
|
||||
this.waypoints = props.waypoints;
|
||||
this.excludedAdId = props.excludedAdId;
|
||||
this.algorithmType = props.algorithmType;
|
||||
this.remoteness = props.remoteness;
|
||||
this.useProportion = props.useProportion;
|
||||
|
@ -72,7 +75,7 @@ export class MatchQuery extends QueryBase {
|
|||
}
|
||||
|
||||
setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => {
|
||||
this.schedule.forEach((day: ScheduleItem) => {
|
||||
this.schedule?.forEach((day: ScheduleItem) => {
|
||||
if (day.margin === undefined) day.margin = defaultMarginDuration;
|
||||
});
|
||||
return this;
|
||||
|
@ -135,6 +138,8 @@ export class MatchQuery extends QueryBase {
|
|||
setDatesAndSchedule = (
|
||||
datetimeTransformer: DateTimeTransformerPort,
|
||||
): MatchQuery => {
|
||||
// no transformation if schedule is not set
|
||||
if (this.schedule === undefined) return this;
|
||||
const initialFromDate: string = this.fromDate;
|
||||
this.fromDate = datetimeTransformer.fromDate(
|
||||
{
|
||||
|
@ -207,7 +212,9 @@ 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,
|
||||
}),
|
||||
})),
|
||||
)
|
||||
).forEach((typedRoute: TypedRoute) => {
|
||||
|
@ -222,7 +229,9 @@ export class MatchQuery extends QueryBase {
|
|||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
throw new Error('Unable to find a route for given waypoints');
|
||||
throw new Error('Unable to find a route for given waypoints', {
|
||||
cause: e,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { Selector } from '../algorithm.abstract';
|
||||
import { Waypoint } from '../../../types/waypoint.type';
|
||||
import { Point } from '../../../types/point.type';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { DateInterval } from '../../../../domain/candidate.types';
|
||||
import { Point } from '../../../types/point.type';
|
||||
import { Waypoint } from '../../../types/waypoint.type';
|
||||
import { Selector } from '../algorithm.abstract';
|
||||
import { ScheduleItem } from '../match.query';
|
||||
|
||||
/**
|
||||
* This class complements the AdRepository prisma service by turning a match query object into a SQL query,
|
||||
* with the assumption that the query is passenger-oriented (i.e. it is up to the driver to go out of his way to pick up the passenger).
|
||||
* The idea is to make a rough filter of the ads in DB to limit the number of ads to be processed more precisely by the application code.
|
||||
* TODO: Converting the query object into a SQL query is a job for the repository implementation
|
||||
* (or anything behind the repository interface),
|
||||
* any logic related to being passenger-oriented should be in the domain layer.
|
||||
* (though it might be difficult to describe generically the search criteria with a query object)
|
||||
*/
|
||||
export class PassengerOrientedSelector extends Selector {
|
||||
select = async (): Promise<CandidateEntity[]> => {
|
||||
const queryStringRoles: QueryStringRole[] = [];
|
||||
|
@ -37,7 +47,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||
id: adEntity.id,
|
||||
role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER,
|
||||
frequency: adEntity.getProps().frequency,
|
||||
dateInterval: {
|
||||
dateInterval: this._fixDateInterval({
|
||||
lowerDate: this._maxDateString(
|
||||
this.query.fromDate,
|
||||
adEntity.getProps().fromDate,
|
||||
|
@ -46,7 +56,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||
this.query.toDate,
|
||||
adEntity.getProps().toDate,
|
||||
),
|
||||
},
|
||||
}),
|
||||
driverWaypoints:
|
||||
adsRole.role == Role.PASSENGER
|
||||
? adEntity.getProps().waypoints
|
||||
|
@ -74,7 +84,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||
driverSchedule:
|
||||
adsRole.role == Role.PASSENGER
|
||||
? adEntity.getProps().schedule
|
||||
: this.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
: this.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day as number,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin as number,
|
||||
|
@ -82,7 +92,7 @@ export class PassengerOrientedSelector extends Selector {
|
|||
passengerSchedule:
|
||||
adsRole.role == Role.DRIVER
|
||||
? adEntity.getProps().schedule
|
||||
: this.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
: this.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day as number,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin as number,
|
||||
|
@ -135,8 +145,8 @@ export class PassengerOrientedSelector extends Selector {
|
|||
[
|
||||
this._whereRole(role),
|
||||
this._whereStrict(),
|
||||
this._whereDate(),
|
||||
this._whereSchedule(role),
|
||||
this._whereDate(role),
|
||||
this._whereExcludedAd(),
|
||||
this._whereAzimuth(),
|
||||
this._whereProportion(role),
|
||||
this._whereRemoteness(role),
|
||||
|
@ -154,102 +164,57 @@ export class PassengerOrientedSelector extends Selector {
|
|||
: `frequency='${Frequency.RECURRENT}'`
|
||||
: '';
|
||||
|
||||
private _whereDate = (): string =>
|
||||
`(\
|
||||
(\
|
||||
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
||||
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
||||
) OR (\
|
||||
"fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
||||
"toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
||||
) OR (\
|
||||
"fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
||||
"toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
||||
) OR (\
|
||||
"fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\
|
||||
"toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\
|
||||
)\
|
||||
)`;
|
||||
/**
|
||||
* Generates the WHERE clause checking that the date range of the query intersects with the range of the ad.
|
||||
* Note that driver dates might not be comparable with passenger dates when the trip is by night or very long.
|
||||
* For this reason, the pickup date is adjusted with the driver duration,
|
||||
* so as to compare with the maximum / minimum driver date that could make sense for the passenger.
|
||||
* This may return more ads than necessary, but they will be filtered out in further processing.
|
||||
*/
|
||||
private _whereDate = (role: Role): string => {
|
||||
const maxFromDate = this._maxFromDate(role);
|
||||
const minToDate = this._minToDate(role);
|
||||
return `("fromDate" <= ${maxFromDate} AND "toDate" >= ${minToDate})`;
|
||||
};
|
||||
|
||||
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)
|
||||
const scheduleDates: Date[] = this._datesBetweenBoundaries(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
);
|
||||
// - then we compare each resulting day of the schedule with each day of calendar,
|
||||
// adding / removing margin depending on the role
|
||||
scheduleDates.map((date: Date) => {
|
||||
this.query.schedule
|
||||
.filter(
|
||||
(scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day,
|
||||
)
|
||||
.map((scheduleItem: ScheduleItem) => {
|
||||
switch (role) {
|
||||
case Role.PASSENGER:
|
||||
schedule.push(this._wherePassengerSchedule(date, scheduleItem));
|
||||
break;
|
||||
case Role.DRIVER:
|
||||
schedule.push(this._whereDriverSchedule(date, scheduleItem));
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (schedule.length > 0) {
|
||||
return ['(', schedule.join(' OR '), ')'].join('');
|
||||
private _maxFromDate = (role: Role): string => {
|
||||
if (role == Role.DRIVER) {
|
||||
//When looking for a passenger, we add the duration of the driver route to the latest toDate
|
||||
//to compute the maximum sensible passenger fromDate, in case the pickup date could be on the next day
|
||||
const querySchedule = this.query.schedule;
|
||||
// When there is no schedule (search whole day), we consider the driver accepts to depart until 23:59
|
||||
const maxScheduleTime =
|
||||
querySchedule === undefined
|
||||
? '23:59'
|
||||
: querySchedule.reduce(
|
||||
(max, s) => (s.time > max ? s.time : max),
|
||||
'00:00',
|
||||
);
|
||||
const [h, m] = maxScheduleTime.split(':');
|
||||
const maxFromDate = new Date(this.query.toDate);
|
||||
maxFromDate.setHours(parseInt(h));
|
||||
maxFromDate.setMinutes(parseInt(m));
|
||||
maxFromDate.setSeconds(this.query.driverRoute!.duration);
|
||||
return `'${maxFromDate.getUTCFullYear()}-${maxFromDate.getUTCMonth() + 1}-${maxFromDate.getUTCDate()}'`;
|
||||
} else {
|
||||
return `'${this.query.toDate}'`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
private _wherePassengerSchedule = (
|
||||
date: Date,
|
||||
scheduleItem: ScheduleItem,
|
||||
): string => {
|
||||
let maxDepartureDatetime: Date = new Date(date);
|
||||
maxDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0]));
|
||||
maxDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1]));
|
||||
maxDepartureDatetime = this._addMargin(
|
||||
maxDepartureDatetime,
|
||||
scheduleItem.margin as number,
|
||||
);
|
||||
// we want the min departure time of the driver to be before the max departure time of the passenger
|
||||
return `make_timestamp(\
|
||||
${maxDepartureDatetime.getUTCFullYear()},\
|
||||
${maxDepartureDatetime.getUTCMonth() + 1},\
|
||||
${maxDepartureDatetime.getUTCDate()},\
|
||||
CAST(EXTRACT(hour from time) as integer),\
|
||||
CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\
|
||||
make_timestamp(\
|
||||
${maxDepartureDatetime.getUTCFullYear()},\
|
||||
${maxDepartureDatetime.getUTCMonth() + 1},\
|
||||
${maxDepartureDatetime.getUTCDate()},${maxDepartureDatetime.getUTCHours()},${maxDepartureDatetime.getUTCMinutes()},0)`;
|
||||
private _minToDate = (role: Role): string => {
|
||||
if (role == Role.PASSENGER) {
|
||||
// When looking for a driver, we look for a toDate that is one day before the fromDate of the query
|
||||
// so that the driver will be able to pick up the passenger even during a long trip that starts the day before
|
||||
const oneDayBeforeFromDate = new Date(this.query.fromDate);
|
||||
oneDayBeforeFromDate.setDate(oneDayBeforeFromDate.getDate() - 1);
|
||||
return `'${oneDayBeforeFromDate.getUTCFullYear()}-${oneDayBeforeFromDate.getUTCMonth() + 1}-${oneDayBeforeFromDate.getUTCDate()}'`;
|
||||
} else {
|
||||
return `'${this.query.fromDate}'`;
|
||||
}
|
||||
};
|
||||
|
||||
private _whereDriverSchedule = (
|
||||
date: Date,
|
||||
scheduleItem: ScheduleItem,
|
||||
): string => {
|
||||
let minDepartureDatetime: Date = new Date(date);
|
||||
minDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0]));
|
||||
minDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1]));
|
||||
minDepartureDatetime = this._addMargin(
|
||||
minDepartureDatetime,
|
||||
-(scheduleItem.margin as number),
|
||||
);
|
||||
// we want the max departure time of the passenger to be after the min departure time of the driver
|
||||
return `make_timestamp(\
|
||||
${minDepartureDatetime.getUTCFullYear()},
|
||||
${minDepartureDatetime.getUTCMonth() + 1},
|
||||
${minDepartureDatetime.getUTCDate()},\
|
||||
CAST(EXTRACT(hour from time) as integer),\
|
||||
CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\
|
||||
make_timestamp(\
|
||||
${minDepartureDatetime.getUTCFullYear()},
|
||||
${minDepartureDatetime.getUTCMonth() + 1},
|
||||
${minDepartureDatetime.getUTCDate()},${minDepartureDatetime.getUTCHours()},${minDepartureDatetime.getUTCMinutes()},0)`;
|
||||
};
|
||||
private _whereExcludedAd = (): string =>
|
||||
this.query.excludedAdId ? `ad.uuid <> '${this.query.excludedAdId}'` : '';
|
||||
|
||||
private _whereAzimuth = (): string => {
|
||||
if (!this.query.useAzimuth) return '';
|
||||
|
@ -310,32 +275,6 @@ export class PassengerOrientedSelector extends Selector {
|
|||
}
|
||||
};
|
||||
|
||||
private _datesBetweenBoundaries = (
|
||||
firstDate: string,
|
||||
lastDate: string,
|
||||
max = 7,
|
||||
): Date[] => {
|
||||
const fromDate: Date = new Date(firstDate);
|
||||
const toDate: Date = new Date(lastDate);
|
||||
const dates: Date[] = [];
|
||||
let count = 0;
|
||||
for (
|
||||
let date = fromDate;
|
||||
date <= toDate;
|
||||
date.setUTCDate(date.getUTCDate() + 1)
|
||||
) {
|
||||
dates.push(new Date(date));
|
||||
count++;
|
||||
if (count == max) break;
|
||||
}
|
||||
return dates;
|
||||
};
|
||||
|
||||
private _addMargin = (date: Date, marginInSeconds: number): Date => {
|
||||
date.setUTCSeconds(marginInSeconds);
|
||||
return date;
|
||||
};
|
||||
|
||||
private _azimuthRange = (
|
||||
azimuth: number,
|
||||
margin: number,
|
||||
|
@ -346,11 +285,26 @@ export class PassengerOrientedSelector extends Selector {
|
|||
azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin,
|
||||
});
|
||||
|
||||
//TODO If the dates are always formatted with '%Y-%m-%d', no conversion to Date is needed
|
||||
private _maxDateString = (date1: string, date2: string): string =>
|
||||
new Date(date1) > new Date(date2) ? date1 : date2;
|
||||
|
||||
private _minDateString = (date1: string, date2: string): string =>
|
||||
new Date(date1) < new Date(date2) ? date1 : date2;
|
||||
|
||||
/**
|
||||
* When a punctual ad matches a punctual query, it may be on a different date than the query
|
||||
* (for routes by night), and the range produced by _minDateString and _maxDateString is not correct.
|
||||
* This function fixes that by inverting the dates if necessary.
|
||||
*/
|
||||
private _fixDateInterval(interval: DateInterval): DateInterval {
|
||||
if (interval.lowerDate > interval.higherDate) {
|
||||
const tmp = interval.lowerDate;
|
||||
interval.lowerDate = interval.higherDate;
|
||||
interval.higherDate = tmp;
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
}
|
||||
|
||||
export type QueryStringRole = {
|
||||
|
|
|
@ -1,14 +1,40 @@
|
|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
|
||||
import { AdProps, CreateAdProps } from './ad.types';
|
||||
import { AdDeletedDomainEvent } from './events/ad-delete.domain-event';
|
||||
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
|
||||
|
||||
export class AdEntity extends AggregateRoot<AdProps> {
|
||||
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;
|
||||
};
|
||||
|
||||
delete(): void {
|
||||
this.addEvent(
|
||||
new AdDeletedDomainEvent({
|
||||
aggregateId: this.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { AdEntity } from './ad.entity';
|
||||
import { Role, UserAd } from './ad.types';
|
||||
import { GeorouterService } from './georouter.service';
|
||||
import {
|
||||
Path,
|
||||
PathCreator,
|
||||
PathType,
|
||||
TypedRoute,
|
||||
} from './path-creator.service';
|
||||
import { Point } from './value-objects/point.value-object';
|
||||
|
||||
export class AdFactory {
|
||||
constructor(private readonly routeProvider: GeorouterService) {}
|
||||
/**
|
||||
* Create an AdEntity (a "matcher ad", that is: the data needed to match an ad with a match query)
|
||||
* from a "user ad" (the data provided by the user).
|
||||
*/
|
||||
public async create(ad: UserAd): Promise<AdEntity> {
|
||||
const roles: Role[] = [];
|
||||
if (ad.driver) roles.push(Role.DRIVER);
|
||||
if (ad.passenger) roles.push(Role.PASSENGER);
|
||||
|
||||
const pathCreator = new PathCreator(
|
||||
roles,
|
||||
ad.waypoints.map((wp) => new Point({ lon: wp.lon, lat: wp.lat })),
|
||||
);
|
||||
|
||||
let typedRoutes: TypedRoute[];
|
||||
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');
|
||||
}
|
||||
|
||||
let driverDistance: number | undefined;
|
||||
let driverDuration: number | undefined;
|
||||
let passengerDistance: number | undefined;
|
||||
let passengerDuration: number | undefined;
|
||||
let points: Point[];
|
||||
let fwdAzimuth: number;
|
||||
let backAzimuth: number;
|
||||
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) => new Point(point));
|
||||
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) => new Point(point));
|
||||
}
|
||||
if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||
if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth;
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new Error('Invalid route');
|
||||
}
|
||||
|
||||
return AdEntity.create({
|
||||
id: ad.id,
|
||||
driver: ad.driver,
|
||||
passenger: ad.passenger,
|
||||
frequency: ad.frequency,
|
||||
fromDate: ad.fromDate,
|
||||
toDate: ad.toDate,
|
||||
schedule: ad.schedule,
|
||||
seatsProposed: ad.seatsProposed,
|
||||
seatsRequested: ad.seatsRequested,
|
||||
strict: ad.strict,
|
||||
waypoints: ad.waypoints,
|
||||
points: points!,
|
||||
driverDistance,
|
||||
driverDuration,
|
||||
passengerDistance,
|
||||
passengerDuration,
|
||||
fwdAzimuth: fwdAzimuth!,
|
||||
backAzimuth: backAzimuth!,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,23 @@
|
|||
import { PointProps } from './value-objects/point.value-object';
|
||||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
||||
|
||||
/**
|
||||
* The data provided by the end-user to publish an ad
|
||||
*/
|
||||
export interface UserAd {
|
||||
id: string;
|
||||
driver: boolean;
|
||||
passenger: boolean;
|
||||
frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
schedule: ScheduleItemProps[];
|
||||
seatsProposed: number;
|
||||
seatsRequested: number;
|
||||
strict: boolean;
|
||||
waypoints: PointProps[];
|
||||
}
|
||||
|
||||
// All properties that an Ad has
|
||||
export interface AdProps {
|
||||
driver: boolean;
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AggregateID,
|
||||
AggregateRoot,
|
||||
ArgumentInvalidException,
|
||||
ValueObject,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Role } from './ad.types';
|
||||
import { CalendarTools } from './calendar-tools.service';
|
||||
import {
|
||||
CandidateProps,
|
||||
CreateCandidateProps,
|
||||
DateInterval,
|
||||
Target,
|
||||
} from './candidate.types';
|
||||
import { ActorTime } from './value-objects/actor-time.value-object';
|
||||
import { Actor } from './value-objects/actor.value-object';
|
||||
import {
|
||||
CarpoolPathItem,
|
||||
CarpoolPathItemProps,
|
||||
} from './value-objects/carpool-path-item.value-object';
|
||||
import { Step, StepProps } from './value-objects/step.value-object';
|
||||
import { ScheduleItem } from './value-objects/schedule-item.value-object';
|
||||
import { Journey } from './value-objects/journey.value-object';
|
||||
import { CalendarTools } from './calendar-tools.service';
|
||||
import { JourneyItem } from './value-objects/journey-item.value-object';
|
||||
import { Actor } from './value-objects/actor.value-object';
|
||||
import { ActorTime } from './value-objects/actor-time.value-object';
|
||||
import { Role } from './ad.types';
|
||||
import { Journey, JourneyProps } from './value-objects/journey.value-object';
|
||||
import {
|
||||
ScheduleItem,
|
||||
ScheduleItemProps,
|
||||
} from './value-objects/schedule-item.value-object';
|
||||
import { Step, StepProps } from './value-objects/step.value-object';
|
||||
|
||||
export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||
protected readonly _id: AggregateID;
|
||||
|
@ -53,13 +62,26 @@ 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());
|
||||
// driver and passenger schedules are eventually mandatory
|
||||
if (!this.props.driverSchedule) this._createDriverSchedule();
|
||||
if (!this.props.passengerSchedule) this._createPassengerSchedule();
|
||||
this.props.journeys = this.props.driverSchedule!.reduce(
|
||||
(accJourneys: JourneyProps[], driverScheduleItem: ScheduleItem) => {
|
||||
try {
|
||||
// first we create the journeys
|
||||
const journey = this._createJourney(driverScheduleItem);
|
||||
// then we filter the ones with invalid pickups
|
||||
if (journey.hasValidPickUp()) {
|
||||
accJourneys.push(journey);
|
||||
}
|
||||
} catch (e) {
|
||||
// irrelevant journeys fall here
|
||||
// eg. no available day for the given date range
|
||||
}
|
||||
return accJourneys;
|
||||
},
|
||||
new Array<JourneyProps>(),
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
|
@ -77,6 +99,49 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||
(1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
|
||||
: false;
|
||||
|
||||
/**
|
||||
* Create the driver schedule based on the passenger schedule
|
||||
*/
|
||||
private _createDriverSchedule = (): void => {
|
||||
const passengerSchedule = new Schedule(
|
||||
this.props.passengerSchedule!,
|
||||
this.props.dateInterval,
|
||||
);
|
||||
this.props.driverSchedule = passengerSchedule
|
||||
.adjust(-this._passengerStartDuration())
|
||||
.unpack().items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the duration to reach the passenger starting point from the driver starting point
|
||||
*/
|
||||
private _passengerStartDuration = (): number => {
|
||||
let passengerStartStepIndex = 0;
|
||||
this.props.carpoolPath?.forEach(
|
||||
(carpoolPathItem: CarpoolPathItem, index: number) => {
|
||||
carpoolPathItem.actors.forEach((actor: Actor) => {
|
||||
if (actor.role == Role.PASSENGER && actor.target == Target.START)
|
||||
passengerStartStepIndex = index;
|
||||
});
|
||||
},
|
||||
);
|
||||
return this.props.steps![passengerStartStepIndex].duration;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the passenger schedule based on the driver schedule
|
||||
*/
|
||||
private _createPassengerSchedule = (): void => {
|
||||
const driverSchedule = new Schedule(
|
||||
this.props.driverSchedule!,
|
||||
this.props.dateInterval,
|
||||
);
|
||||
|
||||
this.props.passengerSchedule = driverSchedule
|
||||
.adjust(this._passengerStartDuration())
|
||||
.unpack().items;
|
||||
};
|
||||
|
||||
private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
|
||||
new Journey({
|
||||
firstDate: CalendarTools.firstDate(
|
||||
|
@ -211,7 +276,7 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||
* Find the passenger schedule item with the minimum duration between a given date and the dates of the passenger schedule
|
||||
*/
|
||||
private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap =>
|
||||
this.props.passengerSchedule
|
||||
(this.props.passengerSchedule as ScheduleItemProps[])
|
||||
// first map the passenger schedule to "real" dates (we use unix epoch date as base)
|
||||
.map(
|
||||
(scheduleItem: ScheduleItem) =>
|
||||
|
@ -250,6 +315,64 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||
|
||||
validate(): void {
|
||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||
if (!this.props.driverSchedule && !this.props.passengerSchedule)
|
||||
throw new ArgumentInvalidException(
|
||||
'at least the driver or the passenger schedule is required',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO Use this class as part of the CandidateEntity aggregate
|
||||
export class Schedule extends ValueObject<{
|
||||
items: ScheduleItemProps[];
|
||||
dateInterval: DateInterval;
|
||||
}> {
|
||||
constructor(items: ScheduleItemProps[], dateInterval: DateInterval) {
|
||||
super({ items, dateInterval });
|
||||
}
|
||||
|
||||
protected validate(): void {}
|
||||
|
||||
/**
|
||||
* Add the given duration to each schedule item
|
||||
* unless the expected new datetime is not possible,
|
||||
* in which case the item is removed from the adjusted schedule
|
||||
* @param duration time increment in seconds (can be negative)
|
||||
* @returns the new adjusted schedule
|
||||
*/
|
||||
adjust(duration: number): Schedule {
|
||||
const newItems = this.props.items.reduce((acc, scheduleItemProps) => {
|
||||
try {
|
||||
const itemDate: Date = CalendarTools.firstDate(
|
||||
scheduleItemProps.day,
|
||||
this.props.dateInterval,
|
||||
);
|
||||
const driverStartDatetime: Date = CalendarTools.datetimeWithSeconds(
|
||||
itemDate,
|
||||
scheduleItemProps.time,
|
||||
duration,
|
||||
);
|
||||
acc.push({
|
||||
day: driverStartDatetime.getUTCDay(),
|
||||
margin: scheduleItemProps.margin,
|
||||
time: this._formatTime(driverStartDatetime),
|
||||
});
|
||||
} catch (e) {
|
||||
// no possible driver date or time
|
||||
// TODO : find a test case !
|
||||
}
|
||||
return acc;
|
||||
}, new Array<ScheduleItemProps>());
|
||||
|
||||
return new Schedule(newItems, this.props.dateInterval);
|
||||
}
|
||||
|
||||
private _formatTime(dateTime: Date) {
|
||||
return (
|
||||
dateTime.getUTCHours().toString().padStart(2, '0') +
|
||||
':' +
|
||||
dateTime.getUTCMinutes().toString().padStart(2, '0')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ export interface CandidateProps {
|
|||
frequency: Frequency;
|
||||
driverWaypoints: PointProps[];
|
||||
passengerWaypoints: PointProps[];
|
||||
driverSchedule: ScheduleItemProps[];
|
||||
passengerSchedule: ScheduleItemProps[];
|
||||
driverSchedule?: ScheduleItemProps[];
|
||||
passengerSchedule?: ScheduleItemProps[];
|
||||
driverDistance: number;
|
||||
driverDuration: number;
|
||||
dateInterval: DateInterval;
|
||||
|
@ -33,8 +33,8 @@ export interface CreateCandidateProps {
|
|||
driverDuration: number;
|
||||
driverWaypoints: PointProps[];
|
||||
passengerWaypoints: PointProps[];
|
||||
driverSchedule: ScheduleItemProps[];
|
||||
passengerSchedule: ScheduleItemProps[];
|
||||
driverSchedule?: ScheduleItemProps[];
|
||||
passengerSchedule?: ScheduleItemProps[];
|
||||
spacetimeDetourRatio: SpacetimeDetourRatio;
|
||||
dateInterval: DateInterval;
|
||||
}
|
||||
|
|
|
@ -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,7 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AdDeletedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<AdDeletedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
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[];
|
||||
};
|
||||
|
||||
export interface GeorouterService {
|
||||
getRoute(request: RouteRequest): Promise<RouteResponse>;
|
||||
}
|
|
@ -2,8 +2,7 @@ import {
|
|||
ArgumentOutOfRangeException,
|
||||
ValueObject,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { Actor, ActorProps } from './actor.value-object';
|
||||
import { Role } from '../ad.types';
|
||||
import { ActorProps } from './actor.value-object';
|
||||
import { Point, PointProps } from './point.value-object';
|
||||
|
||||
/** Note:
|
||||
|
@ -36,12 +35,5 @@ export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> {
|
|||
});
|
||||
if (props.actors.length <= 0)
|
||||
throw new ArgumentOutOfRangeException('at least one actor is required');
|
||||
if (
|
||||
props.actors.filter((actor: Actor) => actor.role == Role.DRIVER).length >
|
||||
1
|
||||
)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
'a carpoolStep can contain only one driver',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,8 +42,8 @@ export class JourneyItem extends ValueObject<JourneyItemProps> {
|
|||
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
||||
) as ActorTime
|
||||
).firstDatetime;
|
||||
return `${driverTime.getHours().toString().padStart(2, '0')}:${driverTime
|
||||
.getMinutes()
|
||||
return `${driverTime.getUTCHours().toString().padStart(2, '0')}:${driverTime
|
||||
.getUTCMinutes()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
};
|
||||
|
|
|
@ -46,6 +46,29 @@ export class Journey extends ValueObject<JourneyProps> {
|
|||
const driverActorTime = passengerDepartureJourneyItem.actorTimes.find(
|
||||
(actorTime: ActorTime) => actorTime.role == Role.DRIVER,
|
||||
) as ActorTime;
|
||||
// TODO : check if the following conditions are even to the ones used in the return
|
||||
// 4 possibilities to be valid :
|
||||
// - 1 : the driver time boundaries are within the passenger time boundaries
|
||||
// - 2 : the max driver time boundary is within the passenger time boundaries
|
||||
// - 3 : the min driver time boundary is within the passenger time boundaries
|
||||
// - 4 : the passenger time boundaries are within the driver time boundaries
|
||||
// return (
|
||||
// // 1
|
||||
// (driverActorTime.firstMinDatetime >=
|
||||
// passengerDepartureActorTime.firstMinDatetime &&
|
||||
// driverActorTime.firstMaxDatetime <=
|
||||
// passengerDepartureActorTime.firstMaxDatetime) ||
|
||||
// // 2 & 4
|
||||
// (driverActorTime.firstMinDatetime <=
|
||||
// passengerDepartureActorTime.firstMinDatetime &&
|
||||
// driverActorTime.firstMaxDatetime >=
|
||||
// passengerDepartureActorTime.firstMinDatetime) ||
|
||||
// // 3
|
||||
// (driverActorTime.firstMinDatetime >=
|
||||
// passengerDepartureActorTime.firstMinDatetime &&
|
||||
// driverActorTime.firstMinDatetime <=
|
||||
// passengerDepartureActorTime.firstMaxDatetime)
|
||||
// );
|
||||
return (
|
||||
(passengerDepartureActorTime.firstMinDatetime <=
|
||||
driverActorTime.firstMaxDatetime &&
|
||||
|
|
|
@ -14,7 +14,7 @@ export interface MatchQueryProps {
|
|||
frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
schedule: ScheduleItemProps[];
|
||||
schedule?: ScheduleItemProps[];
|
||||
seatsProposed: number;
|
||||
seatsRequested: number;
|
||||
strict: boolean;
|
||||
|
@ -50,7 +50,7 @@ export class MatchQuery extends ValueObject<MatchQueryProps> {
|
|||
return this.props.toDate;
|
||||
}
|
||||
|
||||
get schedule(): ScheduleItemProps[] {
|
||||
get schedule(): ScheduleItemProps[] | undefined {
|
||||
return this.props.schedule;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||
import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
||||
import { AdEntity } from '../core/domain/ad.entity';
|
||||
import { AdMapper } from '../ad.mapper';
|
||||
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
|
||||
import { Frequency } from '../core/domain/ad.types';
|
||||
import { SERVICE_NAME } from '@src/app.constants';
|
||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
||||
import { AdMapper } from '../ad.mapper';
|
||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||
import { AdEntity } from '../core/domain/ad.entity';
|
||||
import { Frequency } from '../core/domain/ad.types';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
export type AdModel = {
|
||||
uuid: string;
|
||||
|
@ -26,8 +26,6 @@ export type AdModel = {
|
|||
passengerDistance?: number;
|
||||
fwdAzimuth: number;
|
||||
backAzimuth: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -36,15 +34,26 @@ export type AdModel = {
|
|||
export type AdReadModel = AdModel & {
|
||||
waypoints: string;
|
||||
schedule: ScheduleItemModel[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* The record ready to be sent to the persistence system
|
||||
*/
|
||||
export type AdWriteModel = AdModel & {
|
||||
schedule: {
|
||||
create: ScheduleItemModel[];
|
||||
};
|
||||
schedule: ScheduleWriteModel;
|
||||
};
|
||||
|
||||
export type ScheduleWriteModel = {
|
||||
deleteMany?: PastCreatedFilter;
|
||||
create: ScheduleItemModel[];
|
||||
};
|
||||
|
||||
// used to delete records created in the past,
|
||||
// because the order of `create` and `deleteMany` is not guaranteed
|
||||
export type PastCreatedFilter = {
|
||||
createdAt: { lt: Date };
|
||||
};
|
||||
|
||||
export type AdWriteExtraModel = {
|
||||
|
@ -70,11 +79,15 @@ export type UngroupedAdModel = AdModel &
|
|||
scheduleItemCreatedAt: Date;
|
||||
scheduleItemUpdatedAt: Date;
|
||||
waypoints: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type GroupedAdModel = AdModel & {
|
||||
schedule: ScheduleItemModel[];
|
||||
waypoints: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -169,4 +182,12 @@ export class AdRepository
|
|||
});
|
||||
return adReadModels;
|
||||
};
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
entity: AdEntity,
|
||||
identifier?: string,
|
||||
): Promise<void> {
|
||||
this.updateExtra(id, entity, 'ad', identifier);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ClientGrpc } from '@nestjs/microservices';
|
||||
import { GRPC_GEOROUTER_SERVICE_NAME } from '@src/app.constants';
|
||||
import { Observable, lastValueFrom } from 'rxjs';
|
||||
import { GEOGRAPHY_PACKAGE } from '../ad.di-tokens';
|
||||
import {
|
||||
GeorouterService,
|
||||
RouteRequest,
|
||||
RouteResponse,
|
||||
} from '../core/domain/georouter.service';
|
||||
|
||||
interface GeorouterPort {
|
||||
getRoute(request: RouteRequest): Observable<RouteResponse>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class Georouter implements GeorouterService, OnModuleInit {
|
||||
private georouterService: GeorouterPort;
|
||||
|
||||
constructor(@Inject(GEOGRAPHY_PACKAGE) private readonly client: ClientGrpc) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.georouterService = this.client.getService<GeorouterPort>(
|
||||
GRPC_GEOROUTER_SERVICE_NAME,
|
||||
);
|
||||
}
|
||||
|
||||
getRoute = async (request: RouteRequest): Promise<RouteResponse> => {
|
||||
try {
|
||||
return await lastValueFrom(this.georouterService.getRoute(request));
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { RouteProviderPort } from '../core/application/ports/route-provider.port';
|
||||
import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port';
|
||||
import {
|
||||
AD_GET_BASIC_ROUTE_CONTROLLER,
|
||||
AD_GET_DETAILED_ROUTE_CONTROLLER,
|
||||
} from '../ad.di-tokens';
|
||||
import { Point, Route } from '@modules/geography/core/domain/route.types';
|
||||
|
||||
@Injectable()
|
||||
export class RouteProvider implements RouteProviderPort {
|
||||
constructor(
|
||||
@Inject(AD_GET_BASIC_ROUTE_CONTROLLER)
|
||||
private readonly getBasicRouteController: GetRouteControllerPort,
|
||||
@Inject(AD_GET_DETAILED_ROUTE_CONTROLLER)
|
||||
private readonly getDetailedRouteController: GetRouteControllerPort,
|
||||
) {}
|
||||
|
||||
getBasic = async (waypoints: Point[]): Promise<Route> =>
|
||||
await this.getBasicRouteController.get({
|
||||
waypoints,
|
||||
});
|
||||
|
||||
getDetailed = async (waypoints: Point[]): Promise<Route> =>
|
||||
await this.getDetailedRouteController.get({
|
||||
waypoints,
|
||||
});
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
import { JourneyResponseDto } from './journey.response.dto';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
|
||||
export class MatchResponseDto extends ResponseBase {
|
||||
adId: string;
|
||||
role: string;
|
||||
frequency: string;
|
||||
frequency: Frequency;
|
||||
distance: number;
|
||||
duration: number;
|
||||
initialDistance: number;
|
||||
|
|
|
@ -2,10 +2,10 @@ import {
|
|||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDecimal,
|
||||
IsEnum,
|
||||
IsISO8601,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
Max,
|
||||
|
@ -58,8 +58,9 @@ export class MatchRequestDto {
|
|||
@Type(() => ScheduleItemDto)
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
schedule: ScheduleItemDto[];
|
||||
schedule?: ScheduleItemDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
|
@ -80,6 +81,10 @@ export class MatchRequestDto {
|
|||
@ValidateNested({ each: true })
|
||||
waypoints: WaypointDto[];
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
excludedAdId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AlgorithmType)
|
||||
algorithmType?: AlgorithmType;
|
||||
|
@ -93,7 +98,7 @@ export class MatchRequestDto {
|
|||
useProportion?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
proportion?: number;
|
||||
|
@ -109,13 +114,13 @@ export class MatchRequestDto {
|
|||
azimuthMargin?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
maxDetourDistanceRatio?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
maxDetourDurationRatio?: number;
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { Controller, Inject, UseInterceptors, UsePipes } from '@nestjs/common';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { MatchingPaginatedResponseDto } from '../dtos/matching.paginated.response.dto';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { MatchRequestDto } from './dtos/match.request.dto';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||
import { CacheInterceptor, CacheKey } from '@nestjs/cache-manager';
|
||||
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||
import { Controller, Inject, UseFilters, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod } from '@nestjs/microservices';
|
||||
import { LogCauseExceptionFilter } from '@src/log-cause.exception-filter';
|
||||
import { MatchingPaginatedResponseDto } from '../dtos/matching.paginated.response.dto';
|
||||
import { MatchRequestDto } from './dtos/match.request.dto';
|
||||
|
||||
@UseFilters(LogCauseExceptionFilter)
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
|
@ -24,32 +24,23 @@ export class MatchGrpcController {
|
|||
constructor(
|
||||
private readonly queryBus: QueryBus,
|
||||
@Inject(AD_ROUTE_PROVIDER)
|
||||
private readonly routeProvider: RouteProviderPort,
|
||||
private readonly routeProvider: GeorouterService,
|
||||
private readonly matchMapper: MatchMapper,
|
||||
) {}
|
||||
|
||||
@CacheKey('MatcherServiceMatch')
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@GrpcMethod('MatcherService', 'Match')
|
||||
async match(data: MatchRequestDto): Promise<MatchingPaginatedResponseDto> {
|
||||
try {
|
||||
const matchingResult: MatchingResult = await this.queryBus.execute(
|
||||
new MatchQuery(data, this.routeProvider),
|
||||
);
|
||||
return new MatchingPaginatedResponseDto({
|
||||
id: matchingResult.id,
|
||||
data: matchingResult.matches.map((match: MatchEntity) =>
|
||||
this.matchMapper.toResponse(match),
|
||||
),
|
||||
page: matchingResult.page,
|
||||
perPage: matchingResult.perPage,
|
||||
total: matchingResult.total,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
const matchingResult: MatchingResult = await this.queryBus.execute(
|
||||
new MatchQuery(data, this.routeProvider),
|
||||
);
|
||||
return new MatchingPaginatedResponseDto({
|
||||
id: matchingResult.id,
|
||||
data: matchingResult.matches.map((match: MatchEntity) =>
|
||||
this.matchMapper.toResponse(match),
|
||||
),
|
||||
page: matchingResult.page,
|
||||
perPage: matchingResult.perPage,
|
||||
total: matchingResult.total,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,15 +16,15 @@ message MatchRequest {
|
|||
repeated ScheduleItem schedule = 7;
|
||||
bool strict = 8;
|
||||
repeated Waypoint waypoints = 9;
|
||||
AlgorithmType algorithmType = 10;
|
||||
int32 remoteness = 11;
|
||||
bool useProportion = 12;
|
||||
int32 proportion = 13;
|
||||
bool useAzimuth = 14;
|
||||
int32 azimuthMargin = 15;
|
||||
float maxDetourDistanceRatio = 16;
|
||||
float maxDetourDurationRatio = 17;
|
||||
int32 identifier = 18;
|
||||
string excludedAdId = 10;
|
||||
AlgorithmType algorithmType = 11;
|
||||
int32 remoteness = 12;
|
||||
bool useProportion = 13;
|
||||
float proportion = 14;
|
||||
bool useAzimuth = 15;
|
||||
int32 azimuthMargin = 16;
|
||||
float maxDetourDistanceRatio = 17;
|
||||
float maxDetourDurationRatio = 18;
|
||||
optional int32 page = 19;
|
||||
optional int32 perPage = 20;
|
||||
}
|
||||
|
@ -59,15 +59,16 @@ enum AlgorithmType {
|
|||
message Match {
|
||||
string adId = 1;
|
||||
string role = 2;
|
||||
int32 distance = 3;
|
||||
int32 duration = 4;
|
||||
int32 initialDistance = 5;
|
||||
int32 initialDuration = 6;
|
||||
int32 distanceDetour = 7;
|
||||
int32 durationDetour = 8;
|
||||
double distanceDetourPercentage = 9;
|
||||
double durationDetourPercentage = 10;
|
||||
repeated Journey journeys = 11;
|
||||
Frequency frequency = 3;
|
||||
int32 distance = 4;
|
||||
int32 duration = 5;
|
||||
int32 initialDistance = 6;
|
||||
int32 initialDuration = 7;
|
||||
int32 distanceDetour = 8;
|
||||
int32 durationDetour = 9;
|
||||
double distanceDetourPercentage = 10;
|
||||
double durationDetourPercentage = 11;
|
||||
repeated Journey journeys = 12;
|
||||
}
|
||||
|
||||
message Journey {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||
import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import {
|
||||
AD_DELETED_MESSAGE_HANDLER,
|
||||
AD_DELETED_ROUTING_KEY,
|
||||
} from '@src/app.constants';
|
||||
import { AdReference } from './ad.types';
|
||||
|
||||
@Injectable()
|
||||
export class AdDeletedMessageHandler {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: AD_DELETED_MESSAGE_HANDLER,
|
||||
routingKey: AD_DELETED_ROUTING_KEY,
|
||||
})
|
||||
public async adDeleted(message: string): Promise<void> {
|
||||
try {
|
||||
const deletedAd: AdReference = JSON.parse(message);
|
||||
await this.commandBus.execute(
|
||||
new DeleteAdCommand({
|
||||
id: deletedAd.aggregateId,
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
// do not throw error to acknowledge incoming message
|
||||
// error handling should be done in the command handler, if relevant
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import {
|
||||
AD_UPDATED_MESSAGE_HANDLER,
|
||||
AD_UPDATED_ROUTING_KEY,
|
||||
} from '@src/app.constants';
|
||||
import { Ad } from './ad.types';
|
||||
|
||||
@Injectable()
|
||||
export class AdUpdatedMessageHandler {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: AD_UPDATED_MESSAGE_HANDLER,
|
||||
routingKey: AD_UPDATED_ROUTING_KEY,
|
||||
})
|
||||
public async adUpdated(message: string) {
|
||||
try {
|
||||
const updatedAd: { data: Ad } = JSON.parse(message);
|
||||
await this.commandBus.execute(new UpdateAdCommand(updatedAd.data));
|
||||
} catch (error: any) {
|
||||
// do not throw error to acknowledge incoming message
|
||||
// error handling should be done in the command handler, if relevant
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
|
||||
export type Ad = {
|
||||
export type AdReference = {
|
||||
aggregateId: string;
|
||||
};
|
||||
|
||||
export type Ad = AdReference & {
|
||||
driver: boolean;
|
||||
passenger: boolean;
|
||||
frequency: Frequency;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -64,7 +64,7 @@ export class MatchingMapper
|
|||
toDate: entity.getProps().query.toDate,
|
||||
schedule: entity
|
||||
.getProps()
|
||||
.query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
.query.schedule?.map((scheduleItem: ScheduleItem) => ({
|
||||
day: scheduleItem.day,
|
||||
time: scheduleItem.time,
|
||||
margin: scheduleItem.margin,
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const Nice: PointProps = {
|
||||
lat: 43.7102,
|
||||
lon: 7.262,
|
||||
};
|
||||
|
||||
export const Marseille: PointProps = {
|
||||
lat: 43.2965,
|
||||
lon: 5.3698,
|
||||
};
|
||||
|
||||
export const SaintRaphael: PointProps = {
|
||||
lat: 43.4268,
|
||||
lon: 6.769,
|
||||
};
|
||||
|
||||
export const Toulon: PointProps = {
|
||||
lat: 43.1167,
|
||||
lon: 5.95,
|
||||
};
|
||||
|
||||
export function monday(time: string): ScheduleItemProps {
|
||||
return { day: 1, time: time, margin: 900 };
|
||||
}
|
||||
|
||||
export function wednesday(time: string): ScheduleItemProps {
|
||||
return { day: 3, time: time, margin: 900 };
|
||||
}
|
||||
export function thursday(time: string): ScheduleItemProps {
|
||||
return { day: 4, time: time, margin: 900 };
|
||||
}
|
||||
|
||||
export function weekdays(time: string): ScheduleItemProps[] {
|
||||
return [1, 2, 3, 4, 5].map<ScheduleItemProps>((day) => ({
|
||||
day: day,
|
||||
time: time,
|
||||
margin: 900,
|
||||
}));
|
||||
}
|
||||
|
||||
function createAdPropsDefaults(): CreateAdProps {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
driver: false,
|
||||
passenger: false,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
schedule: [],
|
||||
seatsProposed: 1,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [],
|
||||
points: [],
|
||||
driverDuration: 0,
|
||||
driverDistance: 0,
|
||||
passengerDuration: 0,
|
||||
passengerDistance: 0,
|
||||
fwdAzimuth: 0,
|
||||
backAzimuth: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function driverNiceMarseille(
|
||||
frequency: Frequency,
|
||||
dates: string[],
|
||||
schedule: ScheduleItemProps[],
|
||||
): CreateAdProps {
|
||||
return {
|
||||
...createAdPropsDefaults(),
|
||||
driver: true,
|
||||
frequency: frequency,
|
||||
fromDate: dates[0],
|
||||
toDate: dates[1],
|
||||
schedule: schedule,
|
||||
waypoints: [Nice, Marseille],
|
||||
points: [Nice, SaintRaphael, Toulon, Marseille],
|
||||
driverDuration: 7668,
|
||||
driverDistance: 199000,
|
||||
passengerDuration: 7668,
|
||||
passengerDistance: 199000,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
};
|
||||
}
|
||||
|
||||
export function passengerToulonMarseille(
|
||||
frequency: Frequency,
|
||||
dates: string[],
|
||||
schedule: ScheduleItemProps[],
|
||||
): CreateAdProps {
|
||||
return {
|
||||
...createAdPropsDefaults(),
|
||||
passenger: true,
|
||||
frequency: frequency,
|
||||
fromDate: dates[0],
|
||||
toDate: dates[1],
|
||||
schedule: schedule,
|
||||
waypoints: [Toulon, Marseille],
|
||||
points: [Toulon, Marseille],
|
||||
driverDuration: 2460,
|
||||
driverDistance: 64000,
|
||||
passengerDuration: 2460,
|
||||
passengerDistance: 64000,
|
||||
};
|
||||
}
|
|
@ -1,61 +1,16 @@
|
|||
import {
|
||||
AD_DIRECTION_ENCODER,
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { driverNiceMarseille, wednesday, weekdays } from './ad.fixtures';
|
||||
import { integrationTestingModule } from './integration.setup';
|
||||
|
||||
describe('Ad Repository', () => {
|
||||
let prismaService: PrismaService;
|
||||
let adRepository: AdRepository;
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockLogger = {
|
||||
log: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
EventEmitterModule.forRoot(),
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
],
|
||||
providers: [
|
||||
PrismaService,
|
||||
AdMapper,
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useClass: AdRepository,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
{
|
||||
provide: AD_DIRECTION_ENCODER,
|
||||
useClass: PostgresDirectionEncoder,
|
||||
},
|
||||
],
|
||||
})
|
||||
// disable logging
|
||||
.setLogger(mockLogger)
|
||||
.compile();
|
||||
|
||||
prismaService = module.get<PrismaService>(PrismaService);
|
||||
adRepository = module.get<AdRepository>(AD_REPOSITORY);
|
||||
({ prismaService, adRepository } = await integrationTestingModule());
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -70,60 +25,12 @@ describe('Ad Repository', () => {
|
|||
it('should create a punctual ad', async () => {
|
||||
const beforeCount = await prismaService.ad.count();
|
||||
|
||||
const createAdProps: CreateAdProps = {
|
||||
id: 'b4b56444-f8d3-4110-917c-e37bba77f383',
|
||||
driver: true,
|
||||
passenger: false,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
fromDate: '2023-02-01',
|
||||
toDate: '2023-02-01',
|
||||
schedule: [
|
||||
{
|
||||
day: 3,
|
||||
time: '12:05',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [
|
||||
{
|
||||
lon: 43.7102,
|
||||
lat: 7.262,
|
||||
},
|
||||
{
|
||||
lon: 43.2965,
|
||||
lat: 5.3698,
|
||||
},
|
||||
],
|
||||
points: [
|
||||
{
|
||||
lon: 7.262,
|
||||
lat: 43.7102,
|
||||
},
|
||||
{
|
||||
lon: 6.797838,
|
||||
lat: 43.547031,
|
||||
},
|
||||
{
|
||||
lon: 6.18535,
|
||||
lat: 43.407517,
|
||||
},
|
||||
{
|
||||
lon: 5.3698,
|
||||
lat: 43.2965,
|
||||
},
|
||||
],
|
||||
driverDuration: 7668,
|
||||
driverDistance: 199000,
|
||||
passengerDuration: 7668,
|
||||
passengerDistance: 199000,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
};
|
||||
|
||||
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
||||
const createAdProps = driverNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('08:30')],
|
||||
);
|
||||
const adToCreate = AdEntity.create(createAdProps);
|
||||
await adRepository.insertExtra(adToCreate, 'ad');
|
||||
|
||||
const afterCount = await prismaService.ad.count();
|
||||
|
@ -134,80 +41,13 @@ describe('Ad Repository', () => {
|
|||
it('should create a recurrent ad', async () => {
|
||||
const beforeCount = await prismaService.ad.count();
|
||||
|
||||
const createAdProps: CreateAdProps = {
|
||||
id: 'b4b56444-f8d3-4110-917c-e37bba77f383',
|
||||
driver: true,
|
||||
passenger: false,
|
||||
frequency: Frequency.RECURRENT,
|
||||
fromDate: '2023-02-01',
|
||||
toDate: '2024-01-31',
|
||||
schedule: [
|
||||
{
|
||||
day: 1,
|
||||
time: '08:00',
|
||||
margin: 900,
|
||||
},
|
||||
{
|
||||
day: 2,
|
||||
time: '08:00',
|
||||
margin: 900,
|
||||
},
|
||||
{
|
||||
day: 3,
|
||||
time: '09:00',
|
||||
margin: 900,
|
||||
},
|
||||
{
|
||||
day: 4,
|
||||
time: '08:00',
|
||||
margin: 900,
|
||||
},
|
||||
{
|
||||
day: 5,
|
||||
time: '08:00',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [
|
||||
{
|
||||
lon: 43.7102,
|
||||
lat: 7.262,
|
||||
},
|
||||
{
|
||||
lon: 43.2965,
|
||||
lat: 5.3698,
|
||||
},
|
||||
],
|
||||
points: [
|
||||
{
|
||||
lon: 7.262,
|
||||
lat: 43.7102,
|
||||
},
|
||||
{
|
||||
lon: 6.797838,
|
||||
lat: 43.547031,
|
||||
},
|
||||
{
|
||||
lon: 6.18535,
|
||||
lat: 43.407517,
|
||||
},
|
||||
{
|
||||
lon: 5.3698,
|
||||
lat: 43.2965,
|
||||
},
|
||||
],
|
||||
driverDuration: 7668,
|
||||
driverDistance: 199000,
|
||||
passengerDuration: 7668,
|
||||
passengerDistance: 199000,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
};
|
||||
const createAdProps = driverNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-01', '2024-01-31'],
|
||||
weekdays('08:30'),
|
||||
);
|
||||
|
||||
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
||||
const adToCreate = AdEntity.create(createAdProps);
|
||||
await adRepository.insertExtra(adToCreate, 'ad');
|
||||
|
||||
const afterCount = await prismaService.ad.count();
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import {
|
||||
AD_DIRECTION_ENCODER,
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
export async function integrationTestingModule(): Promise<{
|
||||
prismaService: PrismaService;
|
||||
adRepository: AdRepository;
|
||||
}> {
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
const mockLogger = {
|
||||
log: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
EventEmitterModule.forRoot(),
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
],
|
||||
providers: [
|
||||
PrismaService,
|
||||
AdMapper,
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useClass: AdRepository,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
{
|
||||
provide: AD_DIRECTION_ENCODER,
|
||||
useClass: PostgresDirectionEncoder,
|
||||
},
|
||||
],
|
||||
})
|
||||
.setLogger(mockLogger)
|
||||
.compile();
|
||||
|
||||
return {
|
||||
prismaService: module.get<PrismaService>(PrismaService),
|
||||
adRepository: module.get<AdRepository>(AD_REPOSITORY),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,467 @@
|
|||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector';
|
||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
import { bareMockGeorouter } from '../unit/georouter.mock';
|
||||
import {
|
||||
Marseille,
|
||||
Nice,
|
||||
SaintRaphael,
|
||||
Toulon,
|
||||
driverNiceMarseille,
|
||||
monday,
|
||||
passengerToulonMarseille,
|
||||
thursday,
|
||||
wednesday,
|
||||
} from './ad.fixtures';
|
||||
import { integrationTestingModule } from './integration.setup';
|
||||
function baseMatchQuery(
|
||||
frequency: Frequency,
|
||||
dates: [string, string],
|
||||
scheduleItems: ScheduleItemProps[],
|
||||
waypoints: WaypointDto[],
|
||||
): MatchQuery {
|
||||
return new MatchQuery(
|
||||
{
|
||||
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
|
||||
driver: false,
|
||||
passenger: false,
|
||||
frequency: frequency,
|
||||
fromDate: dates[0],
|
||||
toDate: dates[1],
|
||||
useAzimuth: false,
|
||||
useProportion: false,
|
||||
remoteness: 15000,
|
||||
schedule: scheduleItems,
|
||||
strict: false,
|
||||
waypoints: waypoints,
|
||||
},
|
||||
bareMockGeorouter,
|
||||
);
|
||||
}
|
||||
|
||||
function passengerQueryToulonMarseille(
|
||||
frequency: Frequency,
|
||||
dates: [string, string],
|
||||
scheduleItems: ScheduleItemProps[],
|
||||
): MatchQuery {
|
||||
const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [
|
||||
{ position: 0, ...Toulon },
|
||||
{ position: 1, ...Marseille },
|
||||
]);
|
||||
matchQuery.passenger = true;
|
||||
matchQuery.passengerRoute = {
|
||||
distance: 64000,
|
||||
duration: 2460,
|
||||
points: [Toulon, Marseille],
|
||||
// Not used by this query
|
||||
fwdAzimuth: 0,
|
||||
backAzimuth: 0,
|
||||
distanceAzimuth: 0,
|
||||
};
|
||||
return matchQuery;
|
||||
}
|
||||
|
||||
function driverQueryNiceMarseille(
|
||||
frequency: Frequency,
|
||||
dates: [string, string],
|
||||
scheduleItems: ScheduleItemProps[],
|
||||
): MatchQuery {
|
||||
const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [
|
||||
{ position: 0, ...Nice },
|
||||
{ position: 1, ...Marseille },
|
||||
]);
|
||||
matchQuery.driver = true;
|
||||
matchQuery.driverRoute = {
|
||||
distance: 199000,
|
||||
duration: 7668,
|
||||
points: [Nice, SaintRaphael, Toulon, Marseille],
|
||||
// Not used by this query
|
||||
fwdAzimuth: 0,
|
||||
backAzimuth: 0,
|
||||
distanceAzimuth: 0,
|
||||
};
|
||||
return matchQuery;
|
||||
}
|
||||
|
||||
describe('PassengerOriented selector', () => {
|
||||
let prismaService: PrismaService;
|
||||
let adRepository: AdRepository;
|
||||
|
||||
const insertAd = async (adProps: CreateAdProps): Promise<void> => {
|
||||
const ad = AdEntity.create(adProps);
|
||||
return adRepository.insertExtra(ad, 'ad');
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
({ prismaService, adRepository } = await integrationTestingModule());
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prismaService.$disconnect();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prismaService.ad.deleteMany();
|
||||
});
|
||||
|
||||
describe('select', () => {
|
||||
it('should find a driver that departs on the same day', async () => {
|
||||
await insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
);
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
passengerQueryToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should find a passenger that departs on the same day', async () => {
|
||||
await insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
);
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
driverQueryNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should find a driver that departs the day before', async () => {
|
||||
await insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('23:45')],
|
||||
),
|
||||
);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
passengerQueryToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('01:15')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should find a passenger that departs the day after', async () => {
|
||||
await insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('01:15')],
|
||||
),
|
||||
);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
driverQueryNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('23:45')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should find a driver that departs shortly after midnight', async () => {
|
||||
await insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
//01:30 in Nice is 00:30 in UTC
|
||||
[thursday('01:30')],
|
||||
),
|
||||
);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
passengerQueryToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('03:00')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should find a passenger that departs shortly after midnight', async () => {
|
||||
await insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('03:00')],
|
||||
),
|
||||
);
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
driverQueryNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('01:30')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should NOT find a driver that departs the day after', async () => {
|
||||
await insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('08:30')],
|
||||
),
|
||||
);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
passengerQueryToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should NOT find a passenger that departs the day before', async () => {
|
||||
await insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
);
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
driverQueryNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('08:30')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should find a recurring driver that interesects', async () => {
|
||||
await Promise.all([
|
||||
insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-01', '2023-02-28'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-01', '2023-02-18'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-12', '2023-02-28'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-12', '2023-02-18'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
passengerQueryToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-10', '2023-02-20'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(4);
|
||||
});
|
||||
|
||||
it("should NOT find a recurring driver that doesn't interesect", async () => {
|
||||
await Promise.all([
|
||||
insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-01', '2023-02-10'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-20', '2023-02-28'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
passengerQueryToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-12', '2023-02-18'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should find a recurring passenger that interesects', async () => {
|
||||
await Promise.all([
|
||||
insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-01', '2023-02-28'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-01', '2023-02-18'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-12', '2023-02-28'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-12', '2023-02-18'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
),
|
||||
]);
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
driverQueryNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-10', '2023-02-20'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(4);
|
||||
});
|
||||
|
||||
it("should NOT find a recurring passenger that doesn't interesect", async () => {
|
||||
await Promise.all([
|
||||
insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-01', '2023-02-10'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
),
|
||||
insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-20', '2023-02-28'],
|
||||
[wednesday('10:00')],
|
||||
),
|
||||
),
|
||||
]);
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
driverQueryNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-12', '2023-02-18'],
|
||||
[wednesday('08:30')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should find a borderline driver that departs the day before a recurring query', async () => {
|
||||
await insertAd(
|
||||
driverNiceMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-01', '2023-02-01'],
|
||||
[wednesday('23:45')],
|
||||
),
|
||||
);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
passengerQueryToulonMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-02-02', '2023-02-28'],
|
||||
[monday('13:45'), thursday('01:15')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should find a borderline passenger that departs the day after a recurring query', async () => {
|
||||
await insertAd(
|
||||
passengerToulonMarseille(
|
||||
Frequency.PUNCTUAL,
|
||||
['2023-02-02', '2023-02-02'],
|
||||
[thursday('01:15')],
|
||||
),
|
||||
);
|
||||
|
||||
const passengerOrientedSelector = new PassengerOrientedSelector(
|
||||
driverQueryNiceMarseille(
|
||||
Frequency.RECURRENT,
|
||||
['2023-01-01', '2023-02-01'],
|
||||
[monday('13:45'), wednesday('23:45')],
|
||||
),
|
||||
adRepository,
|
||||
);
|
||||
const candidates = await passengerOrientedSelector.select();
|
||||
expect(candidates.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,52 +1,17 @@
|
|||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
|
||||
const originPointProps: PointProps = {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
};
|
||||
const destinationPointProps: PointProps = {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
};
|
||||
|
||||
const createAdProps: CreateAdProps = {
|
||||
id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
driver: true,
|
||||
passenger: true,
|
||||
fromDate: '2023-06-21',
|
||||
toDate: '2023-06-21',
|
||||
schedule: [
|
||||
{
|
||||
day: 3,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originPointProps, destinationPointProps],
|
||||
driverDistance: 23000,
|
||||
driverDuration: 900,
|
||||
passengerDistance: 23000,
|
||||
passengerDuration: 900,
|
||||
fwdAzimuth: 283,
|
||||
backAzimuth: 93,
|
||||
points: [],
|
||||
};
|
||||
import { createAdProps } from './ad.fixtures';
|
||||
|
||||
describe('Ad entity create', () => {
|
||||
it('should create a new entity', async () => {
|
||||
const ad: AdEntity = AdEntity.create(createAdProps);
|
||||
expect(ad.id.length).toBe(36);
|
||||
expect(ad.getProps().schedule.length).toBe(1);
|
||||
expect(ad.getProps().schedule[0].day).toBe(3);
|
||||
expect(ad.getProps().schedule[0].time).toBe('08:30');
|
||||
expect(ad.getProps().driver).toBeTruthy();
|
||||
expect(ad.getProps().passenger).toBeTruthy();
|
||||
expect(ad.getProps().driverDistance).toBe(23000);
|
||||
describe('create', () => {
|
||||
it('should create a new entity', async () => {
|
||||
const ad: AdEntity = AdEntity.create(createAdProps());
|
||||
expect(ad.id.length).toBe(36);
|
||||
expect(ad.getProps().schedule.length).toBe(1);
|
||||
expect(ad.getProps().schedule[0].day).toBe(3);
|
||||
expect(ad.getProps().schedule[0].time).toBe('08:30');
|
||||
expect(ad.getProps().driver).toBeTruthy();
|
||||
expect(ad.getProps().passenger).toBeTruthy();
|
||||
expect(ad.getProps().driverDistance).toBe(23000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
|
||||
const originPointProps: PointProps = {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
};
|
||||
const destinationPointProps: PointProps = {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
};
|
||||
|
||||
export function createAdProps(): CreateAdProps {
|
||||
return {
|
||||
id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
driver: true,
|
||||
passenger: true,
|
||||
fromDate: '2023-06-21',
|
||||
toDate: '2023-06-21',
|
||||
schedule: [
|
||||
{
|
||||
day: 3,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originPointProps, destinationPointProps],
|
||||
driverDistance: 23000,
|
||||
driverDuration: 900,
|
||||
passengerDistance: 23000,
|
||||
passengerDuration: 900,
|
||||
fwdAzimuth: 283,
|
||||
backAzimuth: 93,
|
||||
points: [],
|
||||
};
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import {
|
||||
Algorithm,
|
||||
Selector,
|
||||
|
@ -9,6 +8,7 @@ import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
|||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
|
@ -29,11 +29,6 @@ const destinationWaypoint: Waypoint = {
|
|||
country: 'France',
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
const matchQuery = new MatchQuery(
|
||||
{
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
|
@ -46,7 +41,7 @@ const matchQuery = new MatchQuery(
|
|||
],
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
mockRouteProvider,
|
||||
bareMockGeorouter,
|
||||
);
|
||||
|
||||
const mockAdRepository: AdRepositoryPort = {
|
||||
|
@ -54,6 +49,7 @@ const mockAdRepository: AdRepositoryPort = {
|
|||
findOneById: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findAllByIds: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateWhere: jest.fn(),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ArgumentInvalidException } from '@mobicoop/ddd-library';
|
||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||
import {
|
||||
|
@ -6,7 +7,10 @@ import {
|
|||
} from '@modules/ad/core/domain/candidate.types';
|
||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
|
||||
import { CarpoolPathItemProps } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
|
||||
import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object';
|
||||
import {
|
||||
Journey,
|
||||
JourneyProps,
|
||||
} from '@modules/ad/core/domain/value-objects/journey.value-object';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||
import { StepProps } from '@modules/ad/core/domain/value-objects/step.value-object';
|
||||
|
@ -33,7 +37,7 @@ const waypointsSet2: PointProps[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const schedule1: ScheduleItemProps[] = [
|
||||
const mondayAt7: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '07:00',
|
||||
|
@ -41,7 +45,7 @@ const schedule1: ScheduleItemProps[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const schedule2: ScheduleItemProps[] = [
|
||||
const mondayAt710: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '07:10',
|
||||
|
@ -49,7 +53,7 @@ const schedule2: ScheduleItemProps[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const schedule3: ScheduleItemProps[] = [
|
||||
const weekdayMornings: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '06:30',
|
||||
|
@ -77,7 +81,7 @@ const schedule3: ScheduleItemProps[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const schedule4: ScheduleItemProps[] = [
|
||||
const schooldayMornings: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '06:50',
|
||||
|
@ -100,7 +104,7 @@ const schedule4: ScheduleItemProps[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const schedule5: ScheduleItemProps[] = [
|
||||
const saturdayNightAndMondayMorning: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 0,
|
||||
time: '00:02',
|
||||
|
@ -113,7 +117,7 @@ const schedule5: ScheduleItemProps[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const schedule6: ScheduleItemProps[] = [
|
||||
const mondayAndSaturdayNights: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 1,
|
||||
time: '23:10',
|
||||
|
@ -126,7 +130,7 @@ const schedule6: ScheduleItemProps[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const schedule7: ScheduleItemProps[] = [
|
||||
const thursdayEvening: ScheduleItemProps[] = [
|
||||
{
|
||||
day: 4,
|
||||
time: '19:00',
|
||||
|
@ -262,8 +266,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
});
|
||||
expect(candidateEntity.id.length).toBe(36);
|
||||
|
@ -282,8 +286,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setCarpoolPath(carpoolPath1);
|
||||
expect(candidateEntity.getProps().carpoolPath).toHaveLength(2);
|
||||
|
@ -302,8 +306,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setMetrics(352688, 14587);
|
||||
expect(candidateEntity.getProps().distance).toBe(352688);
|
||||
|
@ -324,8 +328,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setMetrics(458690, 13980);
|
||||
expect(candidateEntity.isDetourValid()).toBeFalsy();
|
||||
|
@ -343,8 +347,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
}).setMetrics(352368, 18314);
|
||||
expect(candidateEntity.isDetourValid()).toBeFalsy();
|
||||
|
@ -365,8 +369,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule2,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
|
@ -374,6 +378,95 @@ describe('Candidate entity', () => {
|
|||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
});
|
||||
it('should create journeys for a single date without driver schedule', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2023-08-28',
|
||||
higherDate: '2023-08-28',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: undefined,
|
||||
passengerSchedule: mondayAt710,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
// computed driver start time should be 06:49
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCMinutes(),
|
||||
).toBe(49);
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[0].actorTimes[0].firstDatetime.getUTCHours(),
|
||||
).toBe(6);
|
||||
});
|
||||
it('should create journeys for a single date without passenger schedule', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2023-08-28',
|
||||
higherDate: '2023-08-28',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: undefined,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
// computed passenger start time should be 07:20
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(),
|
||||
).toBe(20);
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as JourneyProps[]
|
||||
)[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCHours(),
|
||||
).toBe(7);
|
||||
});
|
||||
it('should throw without driver and passenger schedule', () => {
|
||||
expect(() =>
|
||||
CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2023-08-28',
|
||||
higherDate: '2023-08-28',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: undefined,
|
||||
passengerSchedule: undefined,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys(),
|
||||
).toThrow(ArgumentInvalidException);
|
||||
});
|
||||
it('should create journeys for multiple dates', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
|
@ -387,8 +480,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule3,
|
||||
passengerSchedule: schedule4,
|
||||
driverSchedule: weekdayMornings,
|
||||
passengerSchedule: schooldayMornings,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
|
@ -424,8 +517,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule5,
|
||||
passengerSchedule: schedule6,
|
||||
driverSchedule: saturdayNightAndMondayMorning,
|
||||
passengerSchedule: mondayAndSaturdayNights,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
|
@ -457,6 +550,33 @@ describe('Candidate entity', () => {
|
|||
)[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCMinutes(),
|
||||
).toBe(42);
|
||||
});
|
||||
it('should create a journey for a punctual search from a recurrent driver schedule', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||
role: Role.PASSENGER,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
dateInterval: {
|
||||
lowerDate: '2024-04-01', //This is a Monday
|
||||
higherDate: '2024-04-01',
|
||||
},
|
||||
driverWaypoints: waypointsSet1,
|
||||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: weekdayMornings,
|
||||
passengerSchedule: mondayAt7,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
.setSteps(steps)
|
||||
.createJourneys();
|
||||
expect(candidateEntity.getProps().journeys).toHaveLength(1);
|
||||
expect(
|
||||
(
|
||||
candidateEntity.getProps().journeys as Journey[]
|
||||
)[0].firstDate.getDate(),
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should not create journeys if dates does not match', () => {
|
||||
const candidateEntity: CandidateEntity = CandidateEntity.create({
|
||||
|
@ -471,8 +591,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule7,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: thursdayEvening,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
|
@ -495,8 +615,8 @@ describe('Candidate entity', () => {
|
|||
passengerWaypoints: waypointsSet2,
|
||||
driverDistance: 350145,
|
||||
driverDuration: 13548,
|
||||
driverSchedule: schedule1,
|
||||
passengerSchedule: schedule7,
|
||||
driverSchedule: mondayAt7,
|
||||
passengerSchedule: thursdayEvening,
|
||||
spacetimeDetourRatio,
|
||||
})
|
||||
.setCarpoolPath(carpoolPath2)
|
||||
|
|
|
@ -33,22 +33,4 @@ describe('Carpool Path Item value object', () => {
|
|||
});
|
||||
}).toThrow(ArgumentOutOfRangeException);
|
||||
});
|
||||
it('should throw an exception if actors contains more than one driver', () => {
|
||||
expect(() => {
|
||||
new CarpoolPathItem({
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
actors: [
|
||||
new Actor({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
}),
|
||||
new Actor({
|
||||
role: Role.DRIVER,
|
||||
target: Target.START,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}).toThrow(ArgumentOutOfRangeException);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { 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';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const originWaypoint: PointProps = {
|
||||
lat: 48.689445,
|
||||
|
@ -58,8 +61,8 @@ const mockAdRepository = {
|
|||
}),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest
|
||||
const mockRouteProvider: GeorouterService = {
|
||||
getRoute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
|
@ -93,7 +96,10 @@ const mockRouteProvider: RouteProviderPort = {
|
|||
},
|
||||
],
|
||||
})),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('create-ad.service', () => {
|
||||
|
@ -110,6 +116,10 @@ describe('create-ad.service', () => {
|
|||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
CreateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command';
|
||||
import { DeleteAdService } from '@modules/ad/core/application/commands/delete-ad/delete-ad.service';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { createAdProps } from './ad.fixtures';
|
||||
|
||||
const ad: AdEntity = AdEntity.create(createAdProps());
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => ad),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DeleteAdService', () => {
|
||||
let deleteAdService: DeleteAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
DeleteAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteAdService = module.get<DeleteAdService>(DeleteAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteAdService).toBeDefined();
|
||||
});
|
||||
|
||||
it('should execute the delete logic and delete the ad from the repository', async () => {
|
||||
jest.spyOn(ad, 'delete');
|
||||
await deleteAdService.execute(new DeleteAdCommand(ad.id));
|
||||
expect(ad.delete).toHaveBeenCalled();
|
||||
expect(mockAdRepository.delete).toHaveBeenCalledWith(ad);
|
||||
});
|
||||
});
|
|
@ -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({
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('Match Query value object', () => {
|
|||
expect(matchQueryVO.frequency).toBe(Frequency.PUNCTUAL);
|
||||
expect(matchQueryVO.fromDate).toBe('2023-09-01');
|
||||
expect(matchQueryVO.toDate).toBe('2023-09-01');
|
||||
expect(matchQueryVO.schedule.length).toBe(1);
|
||||
expect(matchQueryVO.schedule?.length).toBe(1);
|
||||
expect(matchQueryVO.seatsProposed).toBe(3);
|
||||
expect(matchQueryVO.seatsRequested).toBe(1);
|
||||
expect(matchQueryVO.strict).toBe(false);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
ConfigurationDomain,
|
||||
ConfigurationDomainGet,
|
||||
Configurator,
|
||||
Domain,
|
||||
GetConfigurationRepositoryPort,
|
||||
KeyType,
|
||||
} from '@mobicoop/configuration-module';
|
||||
import {
|
||||
CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN,
|
||||
|
@ -16,10 +16,10 @@ import {
|
|||
AD_REPOSITORY,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
MATCHING_REPOSITORY,
|
||||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
} 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,
|
||||
|
@ -31,6 +31,9 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
|||
import { Target } from '@modules/ad/core/domain/candidate.types';
|
||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
|
||||
import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer';
|
||||
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
|
||||
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
|
||||
import {
|
||||
MATCH_CONFIG_ALGORITHM,
|
||||
MATCH_CONFIG_AZIMUTH_MARGIN,
|
||||
|
@ -43,6 +46,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 +262,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,
|
||||
},
|
||||
|
@ -344,24 +348,7 @@ const mockConfigurationRepository: GetConfigurationRepositoryPort = {
|
|||
),
|
||||
};
|
||||
|
||||
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
||||
fromDate: jest.fn(),
|
||||
toDate: jest.fn(),
|
||||
day: jest.fn(),
|
||||
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;
|
||||
|
@ -382,9 +369,17 @@ describe('Match Query Handler', () => {
|
|||
provide: AD_CONFIGURATION_REPOSITORY,
|
||||
useValue: mockConfigurationRepository,
|
||||
},
|
||||
{
|
||||
provide: TIMEZONE_FINDER,
|
||||
useClass: TimezoneFinder,
|
||||
},
|
||||
{
|
||||
provide: TIME_CONVERTER,
|
||||
useClass: TimeConverter,
|
||||
},
|
||||
{
|
||||
provide: INPUT_DATETIME_TRANSFORMER,
|
||||
useValue: mockInputDateTimeTransformer,
|
||||
useClass: InputDateTimeTransformer,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||
import {
|
||||
MatchQuery,
|
||||
ScheduleItem,
|
||||
} from '@modules/ad/core/application/queries/match/match.query';
|
||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||
import { simpleMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: Waypoint = {
|
||||
position: 0,
|
||||
|
@ -57,17 +61,10 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
|||
time: jest.fn().mockImplementation(() => '23:05'),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest
|
||||
const mockRouteProvider: GeorouterService = {
|
||||
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 +73,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 +86,6 @@ const mockRouteProvider: RouteProviderPort = {
|
|||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
getDetailed: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Match Query', () => {
|
||||
|
@ -156,9 +138,9 @@ describe('Match Query', () => {
|
|||
expect(matchQuery.maxDetourDurationRatio).toBe(0.3);
|
||||
expect(matchQuery.fromDate).toBe('2023-08-27');
|
||||
expect(matchQuery.toDate).toBe('2023-08-27');
|
||||
expect(matchQuery.schedule[0].day).toBe(0);
|
||||
expect(matchQuery.schedule[0].time).toBe('23:05');
|
||||
expect(matchQuery.schedule[0].margin).toBe(900);
|
||||
expect((matchQuery.schedule as ScheduleItem[])[0].day).toBe(0);
|
||||
expect((matchQuery.schedule as ScheduleItem[])[0].time).toBe('23:05');
|
||||
expect((matchQuery.schedule as ScheduleItem[])[0].margin).toBe(900);
|
||||
});
|
||||
|
||||
it('should set good values for seats', async () => {
|
||||
|
|
|
@ -42,11 +42,10 @@ const matchQuery = new MatchQuery(
|
|||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn().mockImplementation(() => ({
|
||||
getRoute: jest.fn().mockImplementation(() => ({
|
||||
duration: 6500,
|
||||
distance: 89745,
|
||||
})),
|
||||
getDetailed: jest.fn(),
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -55,6 +54,7 @@ const mockMatcherRepository: AdRepositoryPort = {
|
|||
findOneById: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findAllByIds: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
update: jest.fn(),
|
||||
updateWhere: jest.fn(),
|
||||
|
|
|
@ -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,
|
||||
|
@ -46,11 +47,9 @@ const matchQuery = new MatchQuery(
|
|||
],
|
||||
strict: false,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
excludedAdId: '758618c6-dd82-4199-a548-0205161b04d7',
|
||||
},
|
||||
{
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
},
|
||||
bareMockGeorouter,
|
||||
);
|
||||
matchQuery.driverRoute = {
|
||||
distance: 150120,
|
||||
|
@ -73,33 +72,14 @@ matchQuery.driverRoute = {
|
|||
},
|
||||
],
|
||||
};
|
||||
matchQuery.passengerRoute = {
|
||||
distance: 150120,
|
||||
duration: 6540,
|
||||
fwdAzimuth: 276,
|
||||
backAzimuth: 96,
|
||||
distanceAzimuth: 148321,
|
||||
points: [
|
||||
{
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
},
|
||||
{
|
||||
lat: 48.7566,
|
||||
lon: 4.3522,
|
||||
},
|
||||
{
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
},
|
||||
],
|
||||
};
|
||||
matchQuery.passengerRoute = { ...matchQuery.driverRoute };
|
||||
|
||||
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}',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -8,7 +8,13 @@ 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 { Target } from '@modules/ad/core/domain/candidate.types';
|
||||
import {
|
||||
RouteRequest,
|
||||
RouteResponse,
|
||||
} from '@modules/ad/core/domain/georouter.service';
|
||||
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;
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
|
||||
import { UpdateAdService } from '@modules/ad/core/application/commands/update-ad/update-ad.service';
|
||||
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { createAdProps } from './ad.fixtures';
|
||||
|
||||
const mockAdRepository = {
|
||||
update: jest.fn().mockImplementation((id) => {
|
||||
if (id === '42') {
|
||||
throw 'Bad id!';
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
const mockRouteProvider: GeorouterService = {
|
||||
getRoute: jest.fn().mockImplementation(() => ({
|
||||
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 mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('update-ad.service', () => {
|
||||
let updateAdService: UpdateAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
{
|
||||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
UpdateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateAdService = module.get<UpdateAdService>(UpdateAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateAdService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should call the repository update method', async () => {
|
||||
const updateAdCommand = new UpdateAdCommand(createAdProps());
|
||||
await updateAdService.execute(updateAdCommand);
|
||||
expect(mockAdRepository.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit an event when an error occurs', async () => {
|
||||
const commandProps = createAdProps();
|
||||
commandProps.id = '42';
|
||||
const updateAdCommand = new UpdateAdCommand(commandProps);
|
||||
await expect(updateAdService.execute(updateAdCommand)).rejects.toBe(
|
||||
'Bad id!',
|
||||
);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||
|
||||
export const bareMockGeorouter: GeorouterService = {
|
||||
getRoute: jest.fn(),
|
||||
};
|
||||
|
||||
export const simpleMockGeorouter: GeorouterService = {
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import { AdDeletedMessageHandler } from '@modules/ad/interface/message-handlers/ad-deleted.message-handler';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const adDeletedMessage =
|
||||
'{"aggregateId":"4eb6a6af-ecfd-41c3-9118-473a507014d4"}';
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Ad Deleted Message Handler', () => {
|
||||
let adDeletedMessageHandler: AdDeletedMessageHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
AdDeletedMessageHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
adDeletedMessageHandler = module.get<AdDeletedMessageHandler>(
|
||||
AdDeletedMessageHandler,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(adDeletedMessageHandler).toBeDefined();
|
||||
});
|
||||
|
||||
it('should call the delete command', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
await adDeletedMessageHandler.adDeleted(adDeletedMessage);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import { AdUpdatedMessageHandler } from '@modules/ad/interface/message-handlers/ad-updated.message-handler';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const adUpdatedMessage =
|
||||
'{"data": {"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driver":"true","passenger":"true","frequency":"PUNCTUAL","fromDate":"2023-08-18","toDate":"2023-08-18","schedule":[{"day":"5","time":"10:00","margin":"900"}],"seatsProposed":"3","seatsRequested":"1","strict":"false","waypoints":[{"position":"0","houseNumber":"5","street":"rue de la monnaie","locality":"Nancy","postalCode":"54000","country":"France","lon":"48.689445","lat":"6.17651"},{"position":"1","locality":"Paris","postalCode":"75000","country":"France","lon":"48.8566","lat":"2.3522"}]}}';
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Ad Updated Message Handler', () => {
|
||||
let adUpdatedMessageHandler: AdUpdatedMessageHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
AdUpdatedMessageHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
adUpdatedMessageHandler = module.get<AdUpdatedMessageHandler>(
|
||||
AdUpdatedMessageHandler,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(adUpdatedMessageHandler).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update an ad', async () => {
|
||||
await adUpdatedMessageHandler.adUpdated(adUpdatedMessage);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,6 +1,4 @@
|
|||
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';
|
||||
|
@ -15,8 +13,8 @@ import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/matc
|
|||
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { bareMockGeorouter } from '../georouter.mock';
|
||||
|
||||
const originWaypoint: WaypointDto = {
|
||||
position: 0,
|
||||
|
@ -56,136 +54,126 @@ const recurrentMatchRequestDto: MatchRequestDto = {
|
|||
};
|
||||
|
||||
const mockQueryBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
<MatchingResult>{
|
||||
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
matches: [
|
||||
MatchEntity.create({
|
||||
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
||||
role: Role.DRIVER,
|
||||
frequency: Frequency.RECURRENT,
|
||||
distance: 356041,
|
||||
duration: 12647,
|
||||
initialDistance: 349251,
|
||||
initialDuration: 12103,
|
||||
journeys: [
|
||||
{
|
||||
firstDate: new Date('2023-09-01'),
|
||||
lastDate: new Date('2024-08-30'),
|
||||
journeyItems: [
|
||||
new JourneyItem({
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:00'),
|
||||
firstMinDatetime: new Date('2023-09-01 06:45'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:15'),
|
||||
lastDatetime: new Date('2024-08-30 07:00'),
|
||||
lastMinDatetime: new Date('2024-08-30 06:45'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:15'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 48.369445,
|
||||
lon: 6.67487,
|
||||
duration: 2100,
|
||||
distance: 56878,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 07:35'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:20'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:50'),
|
||||
lastDatetime: new Date('2024-08-30 07:35'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:20'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:50'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:32'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:17'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:47'),
|
||||
lastDatetime: new Date('2024-08-30 07:32'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:17'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:47'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.98487,
|
||||
lon: 6.9427,
|
||||
duration: 3840,
|
||||
distance: 76491,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 08:04'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:51'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:19'),
|
||||
lastDatetime: new Date('2024-08-30 08:04'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:51'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:19'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:01'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:46'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:16'),
|
||||
lastDatetime: new Date('2024-08-30 08:01'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:46'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:16'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.365987,
|
||||
lon: 7.02154,
|
||||
duration: 4980,
|
||||
distance: 96475,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:23'),
|
||||
firstMinDatetime: new Date('2023-09-01 08:08'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:38'),
|
||||
lastDatetime: new Date('2024-08-30 08:23'),
|
||||
lastMinDatetime: new Date('2024-08-30 08:08'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:38'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
)
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
const mockRouteProvider: RouteProviderPort = {
|
||||
getBasic: jest.fn(),
|
||||
getDetailed: jest.fn(),
|
||||
execute: jest.fn().mockImplementationOnce(
|
||||
() =>
|
||||
<MatchingResult>{
|
||||
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
matches: [
|
||||
MatchEntity.create({
|
||||
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
||||
role: Role.DRIVER,
|
||||
frequency: Frequency.RECURRENT,
|
||||
distance: 356041,
|
||||
duration: 12647,
|
||||
initialDistance: 349251,
|
||||
initialDuration: 12103,
|
||||
journeys: [
|
||||
{
|
||||
firstDate: new Date('2023-09-01'),
|
||||
lastDate: new Date('2024-08-30'),
|
||||
journeyItems: [
|
||||
new JourneyItem({
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:00'),
|
||||
firstMinDatetime: new Date('2023-09-01 06:45'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:15'),
|
||||
lastDatetime: new Date('2024-08-30 07:00'),
|
||||
lastMinDatetime: new Date('2024-08-30 06:45'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:15'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 48.369445,
|
||||
lon: 6.67487,
|
||||
duration: 2100,
|
||||
distance: 56878,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 07:35'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:20'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:50'),
|
||||
lastDatetime: new Date('2024-08-30 07:35'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:20'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:50'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.START,
|
||||
firstDatetime: new Date('2023-09-01 07:32'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:17'),
|
||||
firstMaxDatetime: new Date('2023-09-01 07:47'),
|
||||
lastDatetime: new Date('2024-08-30 07:32'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:17'),
|
||||
lastMaxDatetime: new Date('2024-08-30 07:47'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.98487,
|
||||
lon: 6.9427,
|
||||
duration: 3840,
|
||||
distance: 76491,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.NEUTRAL,
|
||||
firstDatetime: new Date('2023-09-01 08:04'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:51'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:19'),
|
||||
lastDatetime: new Date('2024-08-30 08:04'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:51'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:19'),
|
||||
}),
|
||||
new ActorTime({
|
||||
role: Role.PASSENGER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:01'),
|
||||
firstMinDatetime: new Date('2023-09-01 07:46'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:16'),
|
||||
lastDatetime: new Date('2024-08-30 08:01'),
|
||||
lastMinDatetime: new Date('2024-08-30 07:46'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:16'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new JourneyItem({
|
||||
lat: 47.365987,
|
||||
lon: 7.02154,
|
||||
duration: 4980,
|
||||
distance: 96475,
|
||||
actorTimes: [
|
||||
new ActorTime({
|
||||
role: Role.DRIVER,
|
||||
target: Target.FINISH,
|
||||
firstDatetime: new Date('2023-09-01 08:23'),
|
||||
firstMinDatetime: new Date('2023-09-01 08:08'),
|
||||
firstMaxDatetime: new Date('2023-09-01 08:38'),
|
||||
lastDatetime: new Date('2024-08-30 08:23'),
|
||||
lastMinDatetime: new Date('2024-08-30 08:08'),
|
||||
lastMaxDatetime: new Date('2024-08-30 08:38'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
const mockMatchMapper = {
|
||||
|
@ -286,7 +274,7 @@ describe('Match Grpc Controller', () => {
|
|||
},
|
||||
{
|
||||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
useValue: bareMockGeorouter,
|
||||
},
|
||||
{
|
||||
provide: MatchMapper,
|
||||
|
@ -322,16 +310,4 @@ describe('Match Grpc Controller', () => {
|
|||
expect(matchingPaginatedResponseDto.perPage).toBe(10);
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await matchGrpcController.match(recurrentMatchRequestDto);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,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
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue