Compare commits
59 Commits
v2.2.4
...
next-relea
Author | SHA1 | Date |
---|---|---|
|
12c237b980 | |
|
f6f9696620 | |
|
5aa4d9e568 | |
|
f6c3204708 | |
|
659c1baea8 | |
|
7a84bff260 | |
|
3d4ff00066 | |
|
3ff5277d5f | |
|
62e5fd56d9 | |
|
c7d4792893 | |
|
5e449ad69a | |
|
51ca6cf9c4 | |
|
be2af64f60 | |
|
9fb7ef2eac | |
|
492bb3ca44 | |
|
e8903099d7 | |
|
b17fc32a12 | |
|
8c7512b6c3 | |
|
15236904e3 | |
|
a7b342c049 | |
|
da4b30350b | |
|
55c7e2b11c | |
|
c52afbb243 | |
|
98d2b521ab | |
|
bbb96cfd36 | |
|
909ef04e69 | |
|
540c63d297 | |
|
c72c64e6da | |
|
8f57dc2c7a | |
|
41073539bf | |
|
483e947d92 | |
|
b13df86745 | |
|
61c1d6ffcb | |
|
fbc0ae2a33 | |
|
b039dbb3bd | |
|
2009355b18 | |
|
5d6547a184 | |
|
40e8b5f733 | |
|
98068d021f | |
|
4e236551ae | |
|
4bd7ca64de | |
|
3e1c4afce3 | |
|
d4a37b237e | |
|
b2cf66139a | |
|
99017b0e55 | |
|
ee0a2cb386 | |
|
f69e8a95f1 | |
|
976a3c3779 | |
|
c85d6fb756 | |
|
e0a4b07733 | |
|
dfe4db8276 | |
|
88a975a8a1 | |
|
263133ec30 | |
|
3d29eb4517 | |
|
d3c305dbce | |
|
4844f07e08 | |
|
5dd02aa0d2 | |
|
a3c503af7e | |
|
66b831d5d2 |
|
@ -8,7 +8,7 @@ HEALTH_SERVICE_PORT=6006
|
|||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=ad"
|
||||
|
||||
# MESSAGE BROKER
|
||||
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
|
||||
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
|
||||
|
||||
|
|
|
@ -4,51 +4,10 @@ stages:
|
|||
- test
|
||||
- build
|
||||
|
||||
##############
|
||||
# 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 ad-tools --env-file ci/.env.ci up -d
|
||||
- sh ci/wait-up.sh
|
||||
- docker-compose -f docker-compose.ci.service.yml -p ad-service --env-file ci/.env.ci up -d
|
||||
- docker exec -t v3-ad-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
|
||||
include:
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
- project: mobicoop/v3/gitlab-templates
|
||||
file:
|
||||
- /ci/service.test-job.yml
|
||||
- /ci/release.build-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 ./
|
||||
|
|
55
README.md
55
README.md
|
@ -56,6 +56,26 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
}
|
||||
```
|
||||
|
||||
- **FindAllByIds** : find all ads for the given ids
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": [
|
||||
"80126a61-d128-4f96-afdb-92e33c75a3e1",
|
||||
"80126a61-d128-4f96-afdb-92e33c75a3e2",
|
||||
"80126a61-d128-4f96-afdb-92e33c75a3e3"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **FindAllByUserId** : find all ads for the given user id
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "80c9bb02-0931-4a1d-bea6-22d358992245"
|
||||
}
|
||||
```
|
||||
|
||||
- **Create** : create an ad
|
||||
|
||||
Punctual driver ad :
|
||||
|
@ -95,7 +115,8 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
"postalCode": "75000",
|
||||
"country": "France"
|
||||
}
|
||||
]
|
||||
],
|
||||
"comment": "I'm flexible with the departure time"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -137,7 +158,8 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
"postalCode": "75000",
|
||||
"country": "France"
|
||||
}
|
||||
]
|
||||
],
|
||||
"comment": "I'm flexible with the departure time"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -187,7 +209,8 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
"postalCode": "75000",
|
||||
"country": "France"
|
||||
}
|
||||
]
|
||||
],
|
||||
"comment": "I'm flexible with the departure time"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -207,12 +230,36 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
|||
- seatsRequested: number of seats requested as passenger (required if `passenger` is true)
|
||||
- strict (boolean): if set to true, allow matching only with similar frequency ads
|
||||
- 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
|
||||
- comment: optional freetext comment / description about the ad
|
||||
|
||||
- **Update** : Replace the content of an ad
|
||||
Accepts the same data as the `Create` function + an ad id, and replace the given ad with the given data.
|
||||
|
||||
- **Delete** : Delete permanently an ad
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "80126a61-d128-4f96-afdb-92e33c75a3e1"
|
||||
}
|
||||
```
|
||||
|
||||
## Messages
|
||||
|
||||
### Handled
|
||||
|
||||
The service listens to these RabbitMQ messages:
|
||||
|
||||
- **matcher-ad.created** (to update the status of pending ads)
|
||||
- **matcher-ad.creation-failed** (to update the status of pending ads)
|
||||
- **user.deleted** (to delete the associated ads)
|
||||
|
||||
### Emitted
|
||||
|
||||
As mentionned earlier, RabbitMQ messages are sent after these events :
|
||||
|
||||
- **Create** (message : the created ad informations)
|
||||
- **ad.created** (message: the created ad information)
|
||||
- **ad.updated** (message: the updated ad information)
|
||||
- **ad.deleted** (message: the id of the deleted ad)
|
||||
|
||||
## Tests / ESLint / Prettier
|
||||
|
||||
|
|
|
@ -9,8 +9,7 @@ DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
|||
RMQ_URI=amqp://v3-ad-broker:5672
|
||||
|
||||
# MESSAGE BROKER
|
||||
BROKER_IMAGE=rabbitmq:3-alpine
|
||||
BROKER_IMAGE=docker.io/rabbitmq:3-alpine
|
||||
|
||||
# POSTGRES
|
||||
POSTGRES_IMAGE=postgres:15.0
|
||||
|
||||
POSTGRES_IMAGE=docker.io/postgres:15.0
|
||||
|
|
|
@ -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:-5006}:${SERVICE_PORT:-5006}
|
||||
- ${HEALTH_SERVICE_PORT:-6006}:${HEALTH_SERVICE_PORT:-6006}
|
||||
- 9226:9229
|
||||
networks:
|
||||
v3-network:
|
||||
aliases:
|
||||
|
|
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mobicoop/ad",
|
||||
"version": "2.2.4",
|
||||
"version": "2.6.0",
|
||||
"description": "Mobicoop V3 Ad",
|
||||
"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",
|
||||
|
@ -24,61 +24,61 @@
|
|||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'",
|
||||
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate deploy'",
|
||||
"migrate:dev": "docker exec v3-ad-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.8",
|
||||
"@grpc/grpc-js": "^1.9.14",
|
||||
"@grpc/proto-loader": "^0.7.10",
|
||||
"@songkeys/nestjs-redis": "^10.0.0",
|
||||
"@mobicoop/ddd-library": "^2.1.1",
|
||||
"@mobicoop/health-module": "^2.3.1",
|
||||
"@mobicoop/message-broker-module": "^2.1.1",
|
||||
"@nestjs/common": "^10.2.7",
|
||||
"@mobicoop/ddd-library": "^2.5.0",
|
||||
"@mobicoop/health-module": "^2.3.2",
|
||||
"@mobicoop/message-broker-module": "^2.1.2",
|
||||
"@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",
|
||||
"@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",
|
||||
"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",
|
||||
"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.6",
|
||||
"@types/node": "20.8.9",
|
||||
"@types/supertest": "^2.0.15",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.0",
|
||||
"@typescript-eslint/parser": "^6.9.0",
|
||||
"@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": [
|
||||
|
@ -96,13 +96,13 @@
|
|||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"rootDir": ".",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
"./src/**/*.(t|j)s"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".constants.ts",
|
||||
|
@ -114,10 +114,10 @@
|
|||
"prisma.service.ts",
|
||||
"main.ts"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"coverageDirectory": "coverage",
|
||||
"moduleNameMapper": {
|
||||
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||
"^@src(.*)": "<rootDir>$1"
|
||||
"^@modules(.*)": "<rootDir>/src/modules/$1",
|
||||
"^@src(.*)": "<rootDir>/src/$1"
|
||||
},
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "Status" AS ENUM ('PENDING', 'VALID', 'INVALID', 'SUSPENDED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ad" ADD COLUMN "status" "Status" NOT NULL DEFAULT 'PENDING';
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "ad" ADD COLUMN "comment" TEXT;
|
|
@ -14,6 +14,7 @@ datasource db {
|
|||
model Ad {
|
||||
uuid String @id @default(uuid()) @db.Uuid
|
||||
userUuid String @db.Uuid
|
||||
status Status @default(PENDING)
|
||||
driver Boolean
|
||||
passenger Boolean
|
||||
frequency Frequency
|
||||
|
@ -26,6 +27,7 @@ model Ad {
|
|||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
waypoints Waypoint[]
|
||||
comment String?
|
||||
|
||||
@@map("ad")
|
||||
}
|
||||
|
@ -66,3 +68,10 @@ enum Frequency {
|
|||
PUNCTUAL
|
||||
RECURRENT
|
||||
}
|
||||
|
||||
enum Status {
|
||||
PENDING
|
||||
VALID
|
||||
INVALID
|
||||
SUSPENDED
|
||||
}
|
||||
|
|
|
@ -5,8 +5,24 @@ export const SERVICE_NAME = 'ad';
|
|||
export const GRPC_PACKAGE_NAME = 'ad';
|
||||
export const GRPC_SERVICE_NAME = 'AdService';
|
||||
|
||||
// messaging
|
||||
// messaging output
|
||||
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
||||
export const AD_UPDATED_ROUTING_KEY = 'ad.updated';
|
||||
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
|
||||
|
||||
// messaging input
|
||||
export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated';
|
||||
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
|
||||
export const MATCHER_AD_CREATED_QUEUE = 'ad.matcher-ad.created';
|
||||
export const MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER =
|
||||
'matcherAdCreationFailed';
|
||||
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
||||
'matcher-ad.creation-failed';
|
||||
export const MATCHER_AD_CREATION_FAILED_QUEUE = 'ad.matcher-ad.creation-failed';
|
||||
|
||||
export const USER_DELETED_MESSAGE_HANDLER = 'userDeleted';
|
||||
export const USER_DELETED_ROUTING_KEY = 'user.deleted';
|
||||
export const USER_DELETED_QUEUE = 'ad.user.deleted';
|
||||
|
||||
// configuration
|
||||
export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set';
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { Mapper } from '@mobicoop/ddd-library';
|
||||
import { AdResponseDto } from './interface/dtos/ad.response.dto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AdEntity } from './core/domain/ad.entity';
|
||||
import {
|
||||
AdWriteModel,
|
||||
AdReadModel,
|
||||
WaypointModel,
|
||||
ScheduleItemModel,
|
||||
} from './infrastructure/ad.repository';
|
||||
import { Frequency } from './core/domain/ad.types';
|
||||
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
|
||||
import { v4 } from 'uuid';
|
||||
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
|
||||
import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens';
|
||||
import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port';
|
||||
import { AdEntity } from './core/domain/ad.entity';
|
||||
import { Frequency, Status } from './core/domain/ad.types';
|
||||
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
|
||||
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
|
||||
import {
|
||||
AdReadModel,
|
||||
AdWriteModel,
|
||||
ScheduleItemModel,
|
||||
ScheduleWriteModel,
|
||||
WaypointModel,
|
||||
WaypointWriteModel,
|
||||
} from './infrastructure/ad.repository';
|
||||
import { AdResponseDto } from './interface/dtos/ad.response.dto';
|
||||
|
||||
/**
|
||||
* Mapper constructs objects that are used in different layers:
|
||||
|
@ -31,21 +33,39 @@ export class AdMapper
|
|||
private readonly outputDatetimeTransformer: DateTimeTransformerPort,
|
||||
) {}
|
||||
|
||||
toPersistence = (entity: AdEntity): AdWriteModel => {
|
||||
toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
|
||||
const copy = entity.getProps();
|
||||
const now = new Date();
|
||||
const record: AdWriteModel = {
|
||||
uuid: copy.id,
|
||||
userUuid: copy.userId,
|
||||
driver: copy.driver as boolean,
|
||||
passenger: copy.passenger as boolean,
|
||||
status: copy.status,
|
||||
frequency: copy.frequency,
|
||||
fromDate: new Date(copy.fromDate),
|
||||
toDate: new Date(copy.toDate),
|
||||
schedule: {
|
||||
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||
schedule: this.toScheduleItemWriteModel(copy.schedule, update),
|
||||
seatsProposed: copy.seatsProposed as number,
|
||||
seatsRequested: copy.seatsRequested as number,
|
||||
strict: copy.strict as boolean,
|
||||
waypoints: this.toWaypointWriteModel(copy.waypoints, update),
|
||||
comment: copy.comment,
|
||||
};
|
||||
return record;
|
||||
};
|
||||
|
||||
toScheduleItemWriteModel = (
|
||||
schedule: ScheduleItemProps[],
|
||||
update?: boolean,
|
||||
): ScheduleWriteModel | undefined => {
|
||||
if (!schedule) {
|
||||
return undefined;
|
||||
}
|
||||
const now = new Date();
|
||||
const record: ScheduleWriteModel = {
|
||||
create: schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||
uuid: v4(),
|
||||
day: scheduleItem.day as number,
|
||||
day: scheduleItem.day,
|
||||
time: new Date(
|
||||
1970,
|
||||
0,
|
||||
|
@ -53,16 +73,29 @@ export class AdMapper
|
|||
parseInt(scheduleItem.time.split(':')[0]),
|
||||
parseInt(scheduleItem.time.split(':')[1]),
|
||||
),
|
||||
margin: scheduleItem.margin as number,
|
||||
margin: scheduleItem.margin,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
},
|
||||
seatsProposed: copy.seatsProposed as number,
|
||||
seatsRequested: copy.seatsRequested as number,
|
||||
strict: copy.strict as boolean,
|
||||
waypoints: {
|
||||
create: copy.waypoints.map((waypoint: WaypointProps) => ({
|
||||
};
|
||||
if (update) {
|
||||
record.deleteMany = {
|
||||
createdAt: { lt: now },
|
||||
};
|
||||
}
|
||||
return record;
|
||||
};
|
||||
|
||||
toWaypointWriteModel = (
|
||||
waypoints: WaypointProps[],
|
||||
update?: boolean,
|
||||
): WaypointWriteModel | undefined => {
|
||||
if (!waypoints) {
|
||||
return undefined;
|
||||
}
|
||||
const now = new Date();
|
||||
const record: WaypointWriteModel = {
|
||||
create: waypoints.map((waypoint: WaypointProps) => ({
|
||||
uuid: v4(),
|
||||
position: waypoint.position,
|
||||
name: waypoint.address.name,
|
||||
|
@ -76,10 +109,12 @@ export class AdMapper
|
|||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
},
|
||||
createdAt: copy.createdAt,
|
||||
updatedAt: copy.updatedAt,
|
||||
};
|
||||
if (update) {
|
||||
record.deleteMany = {
|
||||
createdAt: { lt: now },
|
||||
};
|
||||
}
|
||||
return record;
|
||||
};
|
||||
|
||||
|
@ -92,10 +127,11 @@ export class AdMapper
|
|||
userId: record.userUuid,
|
||||
driver: record.driver,
|
||||
passenger: record.passenger,
|
||||
status: record.status as Status,
|
||||
frequency: record.frequency as Frequency,
|
||||
fromDate: record.fromDate.toISOString().split('T')[0],
|
||||
toDate: record.toDate.toISOString().split('T')[0],
|
||||
schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({
|
||||
schedule: record.schedule?.map((scheduleItem: ScheduleItemModel) => ({
|
||||
day: scheduleItem.day,
|
||||
time: `${scheduleItem.time
|
||||
.getUTCHours()
|
||||
|
@ -109,7 +145,7 @@ export class AdMapper
|
|||
seatsProposed: record.seatsProposed,
|
||||
seatsRequested: record.seatsRequested,
|
||||
strict: record.strict,
|
||||
waypoints: record.waypoints.map((waypoint: WaypointModel) => ({
|
||||
waypoints: record.waypoints?.map((waypoint: WaypointModel) => ({
|
||||
position: waypoint.position,
|
||||
address: {
|
||||
name: waypoint.name,
|
||||
|
@ -124,6 +160,7 @@ export class AdMapper
|
|||
},
|
||||
},
|
||||
})),
|
||||
comment: record.comment,
|
||||
},
|
||||
});
|
||||
return entity;
|
||||
|
@ -135,6 +172,8 @@ export class AdMapper
|
|||
response.userId = props.userId;
|
||||
response.driver = props.driver as boolean;
|
||||
response.passenger = props.passenger as boolean;
|
||||
response.strict = props.strict;
|
||||
response.status = props.status;
|
||||
response.frequency = props.frequency;
|
||||
response.fromDate = this.outputDatetimeTransformer.fromDate(
|
||||
{
|
||||
|
@ -156,7 +195,7 @@ export class AdMapper
|
|||
response.schedule = props.schedule.map(
|
||||
(scheduleItem: ScheduleItemProps) => ({
|
||||
day: this.outputDatetimeTransformer.day(
|
||||
scheduleItem.day as number,
|
||||
scheduleItem.day,
|
||||
{
|
||||
date: props.fromDate,
|
||||
time: scheduleItem.time,
|
||||
|
@ -172,7 +211,7 @@ export class AdMapper
|
|||
},
|
||||
props.frequency,
|
||||
),
|
||||
margin: scheduleItem.margin as number,
|
||||
margin: scheduleItem.margin,
|
||||
}),
|
||||
);
|
||||
response.seatsProposed = props.seatsProposed as number;
|
||||
|
@ -188,6 +227,7 @@ export class AdMapper
|
|||
lon: waypoint.address.coordinates.lon,
|
||||
lat: waypoint.address.coordinates.lat,
|
||||
}));
|
||||
response.comment = props.comment;
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { Module, Provider } from '@nestjs/common';
|
||||
import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
|
||||
import { CqrsModule } from '@nestjs/cqrs';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
|
@ -9,28 +9,70 @@ import {
|
|||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
} from './ad.di-tokens';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
import { AdMapper } from './ad.mapper';
|
||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||
import { TimeConverter } from './infrastructure/time-converter';
|
||||
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
||||
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
||||
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
|
||||
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service';
|
||||
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
|
||||
import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
|
||||
import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service';
|
||||
import { PublishMessageWhenAdIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-deleted.domain-event-handler';
|
||||
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
|
||||
import { PrismaService } from './infrastructure/prisma.service';
|
||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||
import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler';
|
||||
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
||||
import { FindAdsByIdsQueryHandler } from './core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
|
||||
import { FindAdsByUserIdQueryHandler } from './core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||
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 { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
|
||||
import { DeleteAdGrpcController } from './interface/grpc-controllers/delete-ad.grpc.controller';
|
||||
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
||||
import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller';
|
||||
import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
|
||||
import { UpdateAdGrpcController } from './interface/grpc-controllers/update-ad.grpc.controller';
|
||||
import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler';
|
||||
import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler';
|
||||
import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler';
|
||||
|
||||
const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
|
||||
const grpcControllers = [
|
||||
CreateAdGrpcController,
|
||||
UpdateAdGrpcController,
|
||||
DeleteAdGrpcController,
|
||||
FindAdByIdGrpcController,
|
||||
FindAdsByIdsGrpcController,
|
||||
FindAdsByUserIdGrpcController,
|
||||
];
|
||||
|
||||
const messageHandlers = [
|
||||
MatcherAdCreatedMessageHandler,
|
||||
MatcherAdCreationFailedMessageHandler,
|
||||
UserDeletedMessageHandler,
|
||||
];
|
||||
|
||||
const eventHandlers: Provider[] = [
|
||||
PublishMessageWhenAdIsCreatedDomainEventHandler,
|
||||
PublishMessageWhenAdIsUpdatedDomainEventHandler,
|
||||
PublishMessageWhenAdIsDeletedDomainEventHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [CreateAdService];
|
||||
const commandHandlers: Provider[] = [
|
||||
CreateAdService,
|
||||
UpdateAdService,
|
||||
DeleteAdService,
|
||||
DeleteUserAdsService,
|
||||
ValidateAdService,
|
||||
InvalidateAdService,
|
||||
];
|
||||
|
||||
const queryHandlers: Provider[] = [FindAdByIdQueryHandler];
|
||||
const queryHandlers: Provider[] = [
|
||||
FindAdByIdQueryHandler,
|
||||
FindAdsByIdsQueryHandler,
|
||||
FindAdsByUserIdQueryHandler,
|
||||
];
|
||||
|
||||
const mappers: Provider[] = [AdMapper];
|
||||
|
||||
|
@ -72,6 +114,7 @@ const adapters: Provider[] = [
|
|||
imports: [CqrsModule],
|
||||
controllers: [...grpcControllers],
|
||||
providers: [
|
||||
...messageHandlers,
|
||||
...eventHandlers,
|
||||
...commandHandlers,
|
||||
...queryHandlers,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ScheduleItem } from '../../types/schedule-item';
|
||||
import { Waypoint } from '../../types/waypoint';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||
import { Waypoint } from '../../types/waypoint';
|
||||
|
||||
export class CreateAdCommand extends Command {
|
||||
readonly userId: string;
|
||||
|
@ -10,11 +10,12 @@ export class CreateAdCommand extends Command {
|
|||
readonly frequency: Frequency;
|
||||
readonly fromDate: string;
|
||||
readonly toDate: string;
|
||||
readonly schedule: ScheduleItem[];
|
||||
readonly schedule: ScheduleItemProps[];
|
||||
readonly seatsProposed?: number;
|
||||
readonly seatsRequested?: number;
|
||||
readonly strict: boolean;
|
||||
readonly waypoints: Waypoint[];
|
||||
readonly comment?: string;
|
||||
|
||||
constructor(props: CommandProps<CreateAdCommand>) {
|
||||
super(props);
|
||||
|
@ -29,5 +30,6 @@ export class CreateAdCommand extends Command {
|
|||
this.seatsRequested = props.seatsRequested;
|
||||
this.strict = props.strict;
|
||||
this.waypoints = props.waypoints;
|
||||
this.comment = props.comment;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,29 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { CreateAdCommand } from './create-ad.command';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AD_REPOSITORY,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { Waypoint } from '../../types/waypoint';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||
import { ScheduleItem } from '../../types/schedule-item';
|
||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||
import { Waypoint } from '../../types/waypoint';
|
||||
import { CreateAdCommand } from './create-ad.command';
|
||||
|
||||
@CommandHandler(CreateAdCommand)
|
||||
export class CreateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
@Inject(INPUT_DATETIME_TRANSFORMER)
|
||||
private readonly datetimeTransformer: DateTimeTransformerPort,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||
const ad = AdEntity.create({
|
||||
export function createPropsFromCommand(
|
||||
command: CreateAdCommand,
|
||||
datetimeTransformer: DateTimeTransformerPort,
|
||||
) {
|
||||
return {
|
||||
userId: command.userId,
|
||||
driver: command.driver,
|
||||
passenger: command.passenger,
|
||||
frequency: command.frequency,
|
||||
fromDate: this.datetimeTransformer.fromDate(
|
||||
//TODO Shouldn't that kind of logic be in the domain layer?
|
||||
fromDate: datetimeTransformer.fromDate(
|
||||
{
|
||||
date: command.fromDate,
|
||||
time: command.schedule[0].time,
|
||||
|
@ -39,7 +34,7 @@ export class CreateAdService implements ICommandHandler {
|
|||
},
|
||||
command.frequency,
|
||||
),
|
||||
toDate: this.datetimeTransformer.toDate(
|
||||
toDate: datetimeTransformer.toDate(
|
||||
command.toDate,
|
||||
{
|
||||
date: command.fromDate,
|
||||
|
@ -51,8 +46,8 @@ export class CreateAdService implements ICommandHandler {
|
|||
},
|
||||
command.frequency,
|
||||
),
|
||||
schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||
day: this.datetimeTransformer.day(
|
||||
schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||
day: datetimeTransformer.day(
|
||||
scheduleItem.day,
|
||||
{
|
||||
date: command.fromDate,
|
||||
|
@ -64,7 +59,7 @@ export class CreateAdService implements ICommandHandler {
|
|||
},
|
||||
command.frequency,
|
||||
),
|
||||
time: this.datetimeTransformer.time(
|
||||
time: datetimeTransformer.time(
|
||||
{
|
||||
date: command.fromDate,
|
||||
time: scheduleItem.time,
|
||||
|
@ -95,7 +90,23 @@ export class CreateAdService implements ICommandHandler {
|
|||
},
|
||||
},
|
||||
})),
|
||||
});
|
||||
comment: command.comment,
|
||||
};
|
||||
}
|
||||
|
||||
@CommandHandler(CreateAdCommand)
|
||||
export class CreateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
@Inject(INPUT_DATETIME_TRANSFORMER)
|
||||
private readonly datetimeTransformer: DateTimeTransformerPort,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||
const ad = AdEntity.create(
|
||||
createPropsFromCommand(command, this.datetimeTransformer),
|
||||
);
|
||||
|
||||
try {
|
||||
await this.repository.insert(ad);
|
||||
|
|
|
@ -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,7 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class DeleteUserAdsCommand extends Command {
|
||||
constructor(props: CommandProps<DeleteUserAdsCommand>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import {
|
||||
CommandBus,
|
||||
CommandHandler,
|
||||
ICommandHandler,
|
||||
QueryBus,
|
||||
} from '@nestjs/cqrs';
|
||||
import { FindAdsByUserIdQuery } from '../../queries/find-ads-by-user-id/find-ads-by-user-id.query';
|
||||
import { DeleteAdCommand } from '../delete-ad/delete-ad.command';
|
||||
import { DeleteUserAdsCommand } from './delete-user-ads.command';
|
||||
|
||||
@CommandHandler(DeleteUserAdsCommand)
|
||||
export class DeleteUserAdsService implements ICommandHandler {
|
||||
constructor(
|
||||
private readonly queryBus: QueryBus,
|
||||
private readonly commandBus: CommandBus,
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteUserAdsCommand): Promise<void> {
|
||||
const ads: AdEntity[] = await this.queryBus.execute(
|
||||
new FindAdsByUserIdQuery(command.id),
|
||||
);
|
||||
await Promise.all(
|
||||
ads.map((ad) =>
|
||||
this.commandBus.execute(new DeleteAdCommand({ id: ad.id })),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class InvalidateAdCommand extends Command {
|
||||
constructor(props: CommandProps<InvalidateAdCommand>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { InvalidateAdCommand } from './invalidate-ad.command';
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
|
||||
@CommandHandler(InvalidateAdCommand)
|
||||
export class InvalidateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: InvalidateAdCommand): Promise<AggregateID> {
|
||||
const ad: AdEntity = await this.repository.findOneById(command.id, {
|
||||
waypoints: true,
|
||||
schedule: true,
|
||||
});
|
||||
ad.invalid();
|
||||
await this.repository.update(ad.id, ad);
|
||||
return ad.id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { CommandProps } from '@mobicoop/ddd-library';
|
||||
import { CreateAdCommand } from '../create-ad/create-ad.command';
|
||||
|
||||
/**
|
||||
* Ad updates follow the PUT semantics: they replace the entire object.
|
||||
* Therefore the update command extends the create command to inherit the same properties
|
||||
* and re-use the data transformation logic.
|
||||
*/
|
||||
export class UpdateAdCommand extends CreateAdCommand {
|
||||
public adId: string;
|
||||
|
||||
constructor(props: CommandProps<UpdateAdCommand>) {
|
||||
super(props);
|
||||
this.adId = props.adId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
AD_REPOSITORY,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdUpdatedDomainEvent } from '@modules/ad/core/domain/events/ad.domain-event';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||
import { createPropsFromCommand } from '../create-ad/create-ad.service';
|
||||
import { UpdateAdCommand } from './update-ad.command';
|
||||
|
||||
@CommandHandler(UpdateAdCommand)
|
||||
export class UpdateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
@Inject(INPUT_DATETIME_TRANSFORMER)
|
||||
private readonly datetimeTransformer: DateTimeTransformerPort,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateAdCommand): Promise<void> {
|
||||
const ad = await this.repository.findOneById(command.adId, {
|
||||
waypoints: true,
|
||||
schedule: true,
|
||||
});
|
||||
ad.update(createPropsFromCommand(command, this.datetimeTransformer));
|
||||
await this.repository.update(ad.id, ad);
|
||||
this.eventEmitter.emitAsync(
|
||||
AdUpdatedDomainEvent.name,
|
||||
new AdUpdatedDomainEvent(ad),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class ValidateAdCommand extends Command {
|
||||
constructor(props: CommandProps<ValidateAdCommand>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { ValidateAdCommand } from './validate-ad.command';
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
|
||||
@CommandHandler(ValidateAdCommand)
|
||||
export class ValidateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
) {}
|
||||
|
||||
async execute(command: ValidateAdCommand): Promise<AggregateID> {
|
||||
const ad: AdEntity = await this.repository.findOneById(command.id, {
|
||||
waypoints: false,
|
||||
schedule: false,
|
||||
});
|
||||
ad.valid();
|
||||
await this.repository.update(ad.id, ad);
|
||||
return ad.id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { AD_DELETED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { AdDeletedDomainEvent } from '../../domain/events/ad-delete.domain-event';
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenAdIsDeletedDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(AD_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
) {}
|
||||
|
||||
@OnEvent(AdDeletedDomainEvent.name, { async: true, promisify: true })
|
||||
async handle(event: AdDeletedDomainEvent): Promise<void> {
|
||||
this.messagePublisher.publish(
|
||||
AD_DELETED_ROUTING_KEY,
|
||||
JSON.stringify(event),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
|
||||
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-event';
|
||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
|
||||
import { AD_CREATED_ROUTING_KEY } from '@src/app.constants';
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
IntegrationEvent,
|
||||
IntegrationEventProps,
|
||||
MessagePublisherPort,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { AdResponseDto } from '@modules/ad/interface/dtos/ad.response.dto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { AD_UPDATED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { v4 } from 'uuid';
|
||||
import { AdUpdatedDomainEvent } from '../../domain/events/ad.domain-event';
|
||||
|
||||
class AdIntegrationEvent extends IntegrationEvent {
|
||||
readonly data: AdResponseDto;
|
||||
|
||||
constructor(props: IntegrationEventProps<unknown>, data: AdResponseDto) {
|
||||
super(props);
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PublishMessageWhenAdIsUpdatedDomainEventHandler {
|
||||
constructor(
|
||||
@Inject(AD_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
private readonly mapper: AdMapper,
|
||||
) {}
|
||||
|
||||
@OnEvent(AdUpdatedDomainEvent.name, { async: true, promisify: true })
|
||||
async handle(event: AdUpdatedDomainEvent): Promise<void> {
|
||||
this.messagePublisher.publish(
|
||||
AD_UPDATED_ROUTING_KEY,
|
||||
JSON.stringify(
|
||||
new AdIntegrationEvent(
|
||||
{ id: v4(), metadata: event.metadata },
|
||||
this.mapper.toResponse(event.ad),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AdEntity } from '../../../domain/ad.entity';
|
||||
import { FindAdsByIdsQuery } from './find-ads-by-ids.query';
|
||||
|
||||
@QueryHandler(FindAdsByIdsQuery)
|
||||
export class FindAdsByIdsQueryHandler implements IQueryHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
) {}
|
||||
async execute(query: FindAdsByIdsQuery): Promise<AdEntity[]> {
|
||||
return await this.repository.findAllByIds(query.ids, {
|
||||
waypoints: true,
|
||||
schedule: true,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { QueryBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class FindAdsByIdsQuery extends QueryBase {
|
||||
readonly ids: string[];
|
||||
|
||||
constructor(ids: string[]) {
|
||||
super();
|
||||
this.ids = ids;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { AdEntity } from '../../../domain/ad.entity';
|
||||
import { FindAdsByUserIdQuery } from './find-ads-by-user-id.query';
|
||||
|
||||
@QueryHandler(FindAdsByUserIdQuery)
|
||||
export class FindAdsByUserIdQueryHandler implements IQueryHandler {
|
||||
constructor(
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
) {}
|
||||
async execute(query: FindAdsByUserIdQuery): Promise<AdEntity[]> {
|
||||
return await this.repository.findAll(
|
||||
{
|
||||
userUuid: query.userId,
|
||||
},
|
||||
{
|
||||
waypoints: true,
|
||||
schedule: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { QueryBase } from '@mobicoop/ddd-library';
|
||||
|
||||
export class FindAdsByUserIdQuery extends QueryBase {
|
||||
readonly userId: string;
|
||||
|
||||
constructor(userId: string) {
|
||||
super();
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export type ScheduleItem = {
|
||||
day: number;
|
||||
time: string;
|
||||
margin: number;
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import { Address } from './address';
|
||||
|
||||
//TODO Why not use the Waypoint value-object from the domain?
|
||||
export type Waypoint = {
|
||||
position: number;
|
||||
} & Address;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
|
||||
import { v4 } from 'uuid';
|
||||
import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
|
||||
import { AdProps, CreateAdProps } from './ad.types';
|
||||
import { AdProps, CreateAdProps, Status } from './ad.types';
|
||||
import { AdCreatedDomainEvent } from './events/ad-created.domain-event';
|
||||
import { AdDeletedDomainEvent } from './events/ad-delete.domain-event';
|
||||
import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event';
|
||||
import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event';
|
||||
import { AdValidatedDomainEvent } from './events/ad-validated.domain-event';
|
||||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
||||
import { WaypointProps } from './value-objects/waypoint.value-object';
|
||||
|
||||
|
@ -10,10 +14,14 @@ export class AdEntity extends AggregateRoot<AdProps> {
|
|||
|
||||
static create = (create: CreateAdProps): AdEntity => {
|
||||
const id = v4();
|
||||
const props: AdProps = { ...create };
|
||||
const props: AdProps = { ...create, status: Status.PENDING };
|
||||
const ad = new AdEntity({ id, props });
|
||||
ad.addEvent(
|
||||
new AdCreatedDomainEvent({
|
||||
metadata: {
|
||||
correlationId: id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
aggregateId: id,
|
||||
userId: props.userId,
|
||||
driver: props.driver,
|
||||
|
@ -22,9 +30,9 @@ export class AdEntity extends AggregateRoot<AdProps> {
|
|||
fromDate: props.fromDate,
|
||||
toDate: props.toDate,
|
||||
schedule: props.schedule.map((day: ScheduleItemProps) => ({
|
||||
day: day.day as number,
|
||||
day: day.day,
|
||||
time: day.time,
|
||||
margin: day.margin as number,
|
||||
margin: day.margin,
|
||||
})),
|
||||
seatsProposed: props.seatsProposed,
|
||||
seatsRequested: props.seatsRequested,
|
||||
|
@ -40,11 +48,80 @@ export class AdEntity extends AggregateRoot<AdProps> {
|
|||
lon: waypoint.address.coordinates.lon,
|
||||
lat: waypoint.address.coordinates.lat,
|
||||
})),
|
||||
comment: props.comment,
|
||||
}),
|
||||
);
|
||||
return ad;
|
||||
};
|
||||
|
||||
valid = (): AdEntity => {
|
||||
this.props.status = Status.VALID;
|
||||
this.addEvent(
|
||||
new AdValidatedDomainEvent({
|
||||
metadata: {
|
||||
correlationId: this.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
aggregateId: this.id,
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
invalid = (): AdEntity => {
|
||||
this.props.status = Status.INVALID;
|
||||
this.addEvent(
|
||||
new AdInvalidatedDomainEvent({
|
||||
metadata: {
|
||||
correlationId: this.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
aggregateId: this.id,
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
suspend = (): AdEntity => {
|
||||
this.props.status = Status.SUSPENDED;
|
||||
this.addEvent(
|
||||
new AdSuspendedDomainEvent({
|
||||
metadata: {
|
||||
correlationId: this.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
aggregateId: this.id,
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
update = (newProps: CreateAdProps): AdEntity => {
|
||||
this.props.driver = newProps.driver;
|
||||
this.props.passenger = newProps.passenger;
|
||||
this.props.frequency = newProps.frequency;
|
||||
this.props.fromDate = newProps.fromDate;
|
||||
this.props.toDate = newProps.toDate;
|
||||
this.props.seatsProposed = newProps.seatsProposed;
|
||||
this.props.seatsRequested = newProps.seatsRequested;
|
||||
this.props.strict = newProps.strict;
|
||||
this.props.comment = newProps.comment;
|
||||
this.props.schedule = newProps.schedule.map((item) => ({ ...item }));
|
||||
this.props.waypoints = newProps.waypoints.map((wp) => ({ ...wp }));
|
||||
//The ad goes back to pending status until it is validated again
|
||||
this.props.status = Status.PENDING;
|
||||
this.validate();
|
||||
return this;
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,21 +1,6 @@
|
|||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
||||
import { WaypointProps } from './value-objects/waypoint.value-object';
|
||||
|
||||
// All properties that an Ad has
|
||||
export interface AdProps {
|
||||
userId: string;
|
||||
driver: boolean;
|
||||
passenger: boolean;
|
||||
frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
schedule: ScheduleItemProps[];
|
||||
seatsProposed: number;
|
||||
seatsRequested: number;
|
||||
strict: boolean;
|
||||
waypoints: WaypointProps[];
|
||||
}
|
||||
|
||||
// Properties that are needed for an Ad creation
|
||||
export interface CreateAdProps {
|
||||
userId: string;
|
||||
|
@ -29,9 +14,22 @@ export interface CreateAdProps {
|
|||
seatsRequested: number;
|
||||
strict: boolean;
|
||||
waypoints: WaypointProps[];
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
// All properties that an Ad has
|
||||
export interface AdProps extends CreateAdProps {
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export enum Frequency {
|
||||
PUNCTUAL = 'PUNCTUAL',
|
||||
RECURRENT = 'RECURRENT',
|
||||
}
|
||||
|
||||
export enum Status {
|
||||
PENDING = 'PENDING',
|
||||
VALID = 'VALID',
|
||||
INVALID = 'INVALID',
|
||||
SUSPENDED = 'SUSPENDED',
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
import { ScheduleItemProps } from '../value-objects/schedule-item.value-object';
|
||||
|
||||
export class AdCreatedDomainEvent extends DomainEvent {
|
||||
readonly userId: string;
|
||||
|
@ -7,11 +8,12 @@ export class AdCreatedDomainEvent extends DomainEvent {
|
|||
readonly frequency: string;
|
||||
readonly fromDate: string;
|
||||
readonly toDate: string;
|
||||
readonly schedule: ScheduleItem[];
|
||||
readonly schedule: ScheduleItemProps[];
|
||||
readonly seatsProposed: number;
|
||||
readonly seatsRequested: number;
|
||||
readonly strict: boolean;
|
||||
readonly waypoints: Waypoint[];
|
||||
readonly comment?: string;
|
||||
|
||||
constructor(props: DomainEventProps<AdCreatedDomainEvent>) {
|
||||
super(props);
|
||||
|
@ -26,15 +28,10 @@ export class AdCreatedDomainEvent extends DomainEvent {
|
|||
this.seatsRequested = props.seatsRequested;
|
||||
this.strict = props.strict;
|
||||
this.waypoints = props.waypoints;
|
||||
this.comment = props.comment;
|
||||
}
|
||||
}
|
||||
|
||||
export class ScheduleItem {
|
||||
day: number;
|
||||
time: string;
|
||||
margin: number;
|
||||
}
|
||||
|
||||
export class Waypoint {
|
||||
position: number;
|
||||
name?: string;
|
|
@ -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,7 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AdInvalidatedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<AdInvalidatedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AdSuspendedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<AdSuspendedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class AdValidatedDomainEvent extends DomainEvent {
|
||||
constructor(props: DomainEventProps<AdValidatedDomainEvent>) {
|
||||
super(props);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { DomainEvent } from '@mobicoop/ddd-library';
|
||||
import { AdEntity } from '../ad.entity';
|
||||
|
||||
export abstract class AdDomainEvent extends DomainEvent {
|
||||
readonly ad: AdEntity;
|
||||
|
||||
constructor(ad: AdEntity) {
|
||||
super({
|
||||
metadata: {
|
||||
correlationId: ad.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
aggregateId: ad.id,
|
||||
});
|
||||
this.ad = ad;
|
||||
}
|
||||
}
|
||||
|
||||
export class AdUpdatedDomainEvent extends AdDomainEvent {
|
||||
constructor(ad: AdEntity) {
|
||||
super(ad);
|
||||
}
|
||||
}
|
|
@ -6,13 +6,13 @@ import { ValueObject } from '@mobicoop/ddd-library';
|
|||
* */
|
||||
|
||||
export interface ScheduleItemProps {
|
||||
day?: number;
|
||||
day: number;
|
||||
time: string;
|
||||
margin?: number;
|
||||
margin: number;
|
||||
}
|
||||
|
||||
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
|
||||
get day(): number | undefined {
|
||||
get day(): number {
|
||||
return this.props.day;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ export class ScheduleItem extends ValueObject<ScheduleItemProps> {
|
|||
return this.props.time;
|
||||
}
|
||||
|
||||
get margin(): number | undefined {
|
||||
get margin(): number {
|
||||
return this.props.margin;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,44 +1,58 @@
|
|||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { AdEntity } from '../core/domain/ad.entity';
|
||||
import { AdMapper } from '../ad.mapper';
|
||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||
import {
|
||||
LoggerBase,
|
||||
MessagePublisherPort,
|
||||
PrismaRepositoryBase,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
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 { PrismaService } from './prisma.service';
|
||||
|
||||
export type AdBaseModel = {
|
||||
uuid: string;
|
||||
userUuid: string;
|
||||
driver: boolean;
|
||||
passenger: boolean;
|
||||
status: string;
|
||||
frequency: string;
|
||||
fromDate: Date;
|
||||
toDate: Date;
|
||||
seatsProposed: number;
|
||||
seatsRequested: number;
|
||||
strict: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
export type AdReadModel = AdBaseModel & {
|
||||
waypoints: WaypointModel[];
|
||||
schedule: ScheduleItemModel[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type AdWriteModel = AdBaseModel & {
|
||||
waypoints: {
|
||||
create: WaypointModel[];
|
||||
schedule: ScheduleWriteModel | undefined;
|
||||
waypoints: WaypointWriteModel | undefined;
|
||||
};
|
||||
schedule: {
|
||||
|
||||
export type ScheduleWriteModel = {
|
||||
deleteMany?: PastCreatedFilter;
|
||||
create: ScheduleItemModel[];
|
||||
};
|
||||
|
||||
export type WaypointWriteModel = {
|
||||
deleteMany?: PastCreatedFilter;
|
||||
create: WaypointModel[];
|
||||
};
|
||||
|
||||
// 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 ScheduleItemModel = {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { ResponseBase } from '@mobicoop/ddd-library';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { Frequency, Status } from '@modules/ad/core/domain/ad.types';
|
||||
|
||||
export class AdResponseDto extends ResponseBase {
|
||||
userId: string;
|
||||
driver: boolean;
|
||||
passenger: boolean;
|
||||
status: Status;
|
||||
frequency: Frequency;
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
|
@ -27,4 +28,5 @@ export class AdResponseDto extends ResponseBase {
|
|||
lon: number;
|
||||
lat: number;
|
||||
}[];
|
||||
comment?: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { AdResponseDto } from './ad.response.dto';
|
||||
|
||||
export class AdsResponseDto {
|
||||
readonly ads: readonly AdResponseDto[];
|
||||
}
|
|
@ -4,9 +4,10 @@ package ad;
|
|||
|
||||
service AdService {
|
||||
rpc FindOneById(AdById) returns (Ad);
|
||||
rpc FindAll(AdFilter) returns (Ads);
|
||||
rpc FindAllByIds(AdsById) returns (Ads);
|
||||
rpc FindAllByUserId(UserById) returns (Ads);
|
||||
rpc Create(Ad) returns (AdById);
|
||||
rpc Update(Ad) returns (Ad);
|
||||
rpc Update(Ad) returns (Empty);
|
||||
rpc Delete(AdById) returns (Empty);
|
||||
}
|
||||
|
||||
|
@ -14,6 +15,14 @@ message AdById {
|
|||
string id = 1;
|
||||
}
|
||||
|
||||
message UserById {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message AdsById {
|
||||
repeated string ids = 1;
|
||||
}
|
||||
|
||||
message Ad {
|
||||
string id = 1;
|
||||
string userId = 2;
|
||||
|
@ -27,6 +36,7 @@ message Ad {
|
|||
int32 seatsRequested = 10;
|
||||
bool strict = 11;
|
||||
repeated Waypoint waypoints = 12;
|
||||
optional string comment = 13;
|
||||
}
|
||||
|
||||
message ScheduleItem {
|
||||
|
@ -48,18 +58,13 @@ message Waypoint {
|
|||
}
|
||||
|
||||
enum Frequency {
|
||||
UNSPECIFIED = 0;
|
||||
PUNCTUAL = 1;
|
||||
RECURRENT = 2;
|
||||
}
|
||||
|
||||
message AdFilter {
|
||||
int32 page = 1;
|
||||
int32 perPage = 2;
|
||||
}
|
||||
|
||||
message Ads {
|
||||
repeated Ad data = 1;
|
||||
int32 total = 2;
|
||||
repeated Ad ads = 1;
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
DatabaseErrorException,
|
||||
NotFoundException,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { DeleteAdRequestDto } from './dtos/delete-ad.request.dto';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class DeleteAdGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'Delete')
|
||||
async delete(data: DeleteAdRequestDto): Promise<void> {
|
||||
try {
|
||||
await this.commandBus.execute(new DeleteAdCommand(data));
|
||||
} catch (error: any) {
|
||||
if (error instanceof NotFoundException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.NOT_FOUND,
|
||||
message: error.message,
|
||||
});
|
||||
if (error instanceof DatabaseErrorException)
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.INTERNAL,
|
||||
message: error.message,
|
||||
});
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@ import {
|
|||
IsBoolean,
|
||||
IsInt,
|
||||
IsEnum,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
ArrayMinSize,
|
||||
Length,
|
||||
IsUUID,
|
||||
IsArray,
|
||||
IsISO8601,
|
||||
|
@ -78,4 +80,9 @@ export class CreateAdRequestDto {
|
|||
@HasValidPositionIndexes()
|
||||
@ValidateNested({ each: true })
|
||||
waypoints: WaypointDto[];
|
||||
|
||||
@Length(0, 2000)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comment?: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class DeleteAdRequestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ArrayMinSize, IsArray } from 'class-validator';
|
||||
|
||||
export class FindAdsByIdsRequestDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
ids: string[];
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IsString } from 'class-validator';
|
||||
|
||||
export class FindAdsByUserIdRequestDto {
|
||||
@IsString()
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { IsUUID } from 'class-validator';
|
||||
import { CreateAdRequestDto } from './create-ad.request.dto';
|
||||
|
||||
export class UpdateAdRequestDto extends CreateAdRequestDto {
|
||||
@IsUUID(4)
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { FindAdsByIdsRequestDto } from './dtos/find-ads-by-ids.request.dto';
|
||||
import { AdsResponseDto } from '../dtos/ads.response.dto';
|
||||
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class FindAdsByIdsGrpcController {
|
||||
constructor(
|
||||
protected readonly mapper: AdMapper,
|
||||
private readonly queryBus: QueryBus,
|
||||
) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'FindAllByIds')
|
||||
async findAllByIds(data: FindAdsByIdsRequestDto): Promise<AdsResponseDto> {
|
||||
try {
|
||||
const ads: AdEntity[] = await this.queryBus.execute(
|
||||
new FindAdsByIdsQuery(data.ids),
|
||||
);
|
||||
return {
|
||||
ads: ads.map((ad: AdEntity) => this.mapper.toResponse(ad)),
|
||||
};
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { AdsResponseDto } from '../dtos/ads.response.dto';
|
||||
import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query';
|
||||
import { FindAdsByUserIdRequestDto } from './dtos/find-ads-by-user-id.request.dto';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class FindAdsByUserIdGrpcController {
|
||||
constructor(
|
||||
protected readonly mapper: AdMapper,
|
||||
private readonly queryBus: QueryBus,
|
||||
) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'FindAllByUserId')
|
||||
async findAllByUserId(
|
||||
data: FindAdsByUserIdRequestDto,
|
||||
): Promise<AdsResponseDto> {
|
||||
try {
|
||||
const ads: AdEntity[] = await this.queryBus.execute(
|
||||
new FindAdsByUserIdQuery(data.id),
|
||||
);
|
||||
return {
|
||||
ads: ads.map((ad: AdEntity) => this.mapper.toResponse(ad)),
|
||||
};
|
||||
} catch (e) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.UNKNOWN,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
NotFoundException,
|
||||
RpcExceptionCode,
|
||||
RpcValidationPipe,
|
||||
} from '@mobicoop/ddd-library';
|
||||
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
|
||||
import { Controller, UsePipes } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
|
||||
import { UpdateAdRequestDto } from './dtos/update-ad.request.dto';
|
||||
|
||||
@UsePipes(
|
||||
new RpcValidationPipe({
|
||||
whitelist: false,
|
||||
forbidUnknownValues: false,
|
||||
}),
|
||||
)
|
||||
@Controller()
|
||||
export class UpdateAdGrpcController {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@GrpcMethod(GRPC_SERVICE_NAME, 'Update')
|
||||
async update(data: UpdateAdRequestDto): Promise<void> {
|
||||
try {
|
||||
const cmdProps = {
|
||||
adId: data.id,
|
||||
...data,
|
||||
};
|
||||
delete (cmdProps as { id?: string }).id;
|
||||
|
||||
await this.commandBus.execute(new UpdateAdCommand(cmdProps));
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundException) {
|
||||
throw new RpcException({
|
||||
code: RpcExceptionCode.NOT_FOUND,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { MATCHER_AD_CREATED_MESSAGE_HANDLER } from '@src/app.constants';
|
||||
import { MatcherAdCreatedIntegrationEvent } from './matcher-ad.types';
|
||||
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
|
||||
|
||||
@Injectable()
|
||||
export class MatcherAdCreatedMessageHandler {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: MATCHER_AD_CREATED_MESSAGE_HANDLER,
|
||||
})
|
||||
public async matcherAdCreated(message: string) {
|
||||
try {
|
||||
const matcherAdCreatedIntegrationEvent: MatcherAdCreatedIntegrationEvent =
|
||||
JSON.parse(message);
|
||||
await this.commandBus.execute(
|
||||
new ValidateAdCommand({
|
||||
id: matcherAdCreatedIntegrationEvent.id,
|
||||
}),
|
||||
);
|
||||
} 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,29 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER } from '@src/app.constants';
|
||||
import { MatcherAdCreationFailedIntegrationEvent } from './matcher-ad.types';
|
||||
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
|
||||
|
||||
@Injectable()
|
||||
export class MatcherAdCreationFailedMessageHandler {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER,
|
||||
})
|
||||
public async matcherAdCreationFailed(message: string) {
|
||||
try {
|
||||
const matcherAdCreationFailedIntegrationEvent: MatcherAdCreationFailedIntegrationEvent =
|
||||
JSON.parse(message);
|
||||
await this.commandBus.execute(
|
||||
new InvalidateAdCommand({
|
||||
id: matcherAdCreationFailedIntegrationEvent.id,
|
||||
}),
|
||||
);
|
||||
} 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,4 @@
|
|||
import { IntegrationEvent } from '@mobicoop/ddd-library';
|
||||
|
||||
export type MatcherAdCreatedIntegrationEvent = IntegrationEvent;
|
||||
export type MatcherAdCreationFailedIntegrationEvent = IntegrationEvent;
|
|
@ -0,0 +1,23 @@
|
|||
import { IntegrationEvent } from '@mobicoop/ddd-library';
|
||||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||
import { DeleteUserAdsCommand } from '@modules/ad/core/application/commands/delete-user-ads/delete-user-ads.command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { USER_DELETED_MESSAGE_HANDLER } from '@src/app.constants';
|
||||
|
||||
type UserDeletedEvent = IntegrationEvent;
|
||||
|
||||
@Injectable()
|
||||
export class UserDeletedMessageHandler {
|
||||
constructor(private readonly commandBus: CommandBus) {}
|
||||
|
||||
@RabbitSubscribe({
|
||||
name: USER_DELETED_MESSAGE_HANDLER,
|
||||
})
|
||||
public async userDeleted(message: string) {
|
||||
const deletedUser: UserDeletedEvent = JSON.parse(message);
|
||||
await this.commandBus.execute(
|
||||
new DeleteUserAdsCommand({ id: deletedUser.id }),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,24 @@
|
|||
import { Module, Provider } from '@nestjs/common';
|
||||
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { SERVICE_NAME } from '@src/app.constants';
|
||||
import {
|
||||
MessageBrokerModule,
|
||||
MessageBrokerModuleOptions,
|
||||
MessageBrokerPublisher,
|
||||
} from '@mobicoop/message-broker-module';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
MATCHER_AD_CREATED_MESSAGE_HANDLER,
|
||||
MATCHER_AD_CREATED_QUEUE,
|
||||
MATCHER_AD_CREATED_ROUTING_KEY,
|
||||
MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER,
|
||||
MATCHER_AD_CREATION_FAILED_QUEUE,
|
||||
MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
|
||||
SERVICE_NAME,
|
||||
USER_DELETED_MESSAGE_HANDLER,
|
||||
USER_DELETED_QUEUE,
|
||||
USER_DELETED_ROUTING_KEY,
|
||||
} from '@src/app.constants';
|
||||
|
||||
const imports = [
|
||||
MessageBrokerModule.forRootAsync({
|
||||
|
@ -24,6 +35,20 @@ const imports = [
|
|||
) as boolean,
|
||||
},
|
||||
name: SERVICE_NAME,
|
||||
handlers: {
|
||||
[MATCHER_AD_CREATED_MESSAGE_HANDLER]: {
|
||||
routingKey: MATCHER_AD_CREATED_ROUTING_KEY,
|
||||
queue: MATCHER_AD_CREATED_QUEUE,
|
||||
},
|
||||
[MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER]: {
|
||||
routingKey: MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
|
||||
queue: MATCHER_AD_CREATION_FAILED_QUEUE,
|
||||
},
|
||||
[USER_DELETED_MESSAGE_HANDLER]: {
|
||||
routingKey: USER_DELETED_ROUTING_KEY,
|
||||
queue: USER_DELETED_QUEUE,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
|
|
@ -2,7 +2,7 @@ import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens';
|
|||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { Frequency, Status } from '@modules/ad/core/domain/ad.types';
|
||||
import {
|
||||
AdReadModel,
|
||||
AdWriteModel,
|
||||
|
@ -17,6 +17,7 @@ const adEntity: AdEntity = new AdEntity({
|
|||
userId: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
|
||||
driver: false,
|
||||
passenger: true,
|
||||
status: Status.PENDING,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
fromDate: '2023-06-21',
|
||||
toDate: '2023-06-21',
|
||||
|
@ -67,6 +68,7 @@ const adReadModel: AdReadModel = {
|
|||
userUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
|
||||
driver: false,
|
||||
passenger: true,
|
||||
status: Status.PENDING,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
fromDate: new Date('2023-06-21'),
|
||||
toDate: new Date('2023-06-21'),
|
||||
|
@ -109,6 +111,7 @@ const adReadModel: AdReadModel = {
|
|||
strict: false,
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
comment: '',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
@ -142,9 +145,9 @@ describe('Ad Mapper', () => {
|
|||
|
||||
it('should map domain entity to persistence data', async () => {
|
||||
const mapped: AdWriteModel = adMapper.toPersistence(adEntity);
|
||||
expect(mapped.waypoints.create[0].uuid.length).toBe(36);
|
||||
expect(mapped.waypoints.create[1].uuid.length).toBe(36);
|
||||
expect(mapped.schedule.create.length).toBe(1);
|
||||
expect(mapped.waypoints?.create[0].uuid.length).toBe(36);
|
||||
expect(mapped.waypoints?.create[1].uuid.length).toBe(36);
|
||||
expect(mapped.schedule?.create.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should map persisted data to domain entity', async () => {
|
|
@ -0,0 +1,10 @@
|
|||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||
|
||||
export function mockInputDateTimeTransformer(): DateTimeTransformerPort {
|
||||
return {
|
||||
fromDate: jest.fn(),
|
||||
toDate: jest.fn(),
|
||||
day: jest.fn(),
|
||||
time: jest.fn(),
|
||||
};
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import {
|
||||
CreateAdProps,
|
||||
Frequency,
|
||||
Status,
|
||||
} from '@modules/ad/core/domain/ad.types';
|
||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||
|
||||
const originWaypointProps: WaypointProps = {
|
||||
|
@ -35,6 +39,7 @@ const baseCreateAdProps = {
|
|||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originWaypointProps, destinationWaypointProps],
|
||||
comment: "J'accepte les chiens mais pas les chats",
|
||||
};
|
||||
const punctualCreateAdProps = {
|
||||
fromDate: '2023-06-21',
|
||||
|
@ -124,11 +129,15 @@ describe('Ad entity create', () => {
|
|||
punctualPassengerCreateAdProps,
|
||||
);
|
||||
expect(punctualPassengerAd.id.length).toBe(36);
|
||||
expect(punctualPassengerAd.getProps().status).toBe(Status.PENDING);
|
||||
expect(punctualPassengerAd.getProps().schedule.length).toBe(1);
|
||||
expect(punctualPassengerAd.getProps().schedule[0].day).toBe(3);
|
||||
expect(punctualPassengerAd.getProps().schedule[0].time).toBe('08:30');
|
||||
expect(punctualPassengerAd.getProps().driver).toBeFalsy();
|
||||
expect(punctualPassengerAd.getProps().passenger).toBeTruthy();
|
||||
expect(punctualPassengerAd.getProps().comment).toBe(
|
||||
"J'accepte les chiens mais pas les chats",
|
||||
);
|
||||
});
|
||||
it('should create a new punctual driver ad entity', async () => {
|
||||
const punctualDriverAd: AdEntity = AdEntity.create(
|
||||
|
@ -191,3 +200,33 @@ describe('Ad entity create', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ad entity validate status', () => {
|
||||
it('should validate status of a pending ad entity', async () => {
|
||||
const punctualPassengerAd: AdEntity = AdEntity.create(
|
||||
punctualPassengerCreateAdProps,
|
||||
);
|
||||
punctualPassengerAd.valid();
|
||||
expect(punctualPassengerAd.getProps().status).toBe(Status.VALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ad entity invalidate status', () => {
|
||||
it('should invalidate status of a pending ad entity', async () => {
|
||||
const punctualPassengerAd: AdEntity = AdEntity.create(
|
||||
punctualPassengerCreateAdProps,
|
||||
);
|
||||
punctualPassengerAd.invalid();
|
||||
expect(punctualPassengerAd.getProps().status).toBe(Status.INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ad entity suspend status', () => {
|
||||
it('should suspend status of a pending ad entity', async () => {
|
||||
const punctualPassengerAd: AdEntity = AdEntity.create(
|
||||
punctualPassengerCreateAdProps,
|
||||
);
|
||||
punctualPassengerAd.suspend();
|
||||
expect(punctualPassengerAd.getProps().status).toBe(Status.SUSPENDED);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||
|
||||
const originWaypointProps: WaypointProps = {
|
||||
position: 0,
|
||||
address: {
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
},
|
||||
},
|
||||
};
|
||||
const destinationWaypointProps: WaypointProps = {
|
||||
position: 1,
|
||||
address: {
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
},
|
||||
},
|
||||
};
|
||||
const baseCreateAdProps = {
|
||||
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originWaypointProps, destinationWaypointProps],
|
||||
};
|
||||
const punctualCreateAdProps = {
|
||||
fromDate: '2023-06-22',
|
||||
toDate: '2023-06-22',
|
||||
schedule: [
|
||||
{
|
||||
day: 4,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
};
|
||||
|
||||
export function punctualPassengerCreateAdProps(): CreateAdProps {
|
||||
return {
|
||||
...baseCreateAdProps,
|
||||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
};
|
||||
}
|
|
@ -1,18 +1,17 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AD_REPOSITORY,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
||||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { ConflictException } from '@mobicoop/ddd-library';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||
import { 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 { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { mockInputDateTimeTransformer } from '../ad.mocks';
|
||||
|
||||
const originWaypoint: WaypointDto = {
|
||||
position: 0,
|
||||
|
@ -64,13 +63,6 @@ const mockAdRepository = {
|
|||
}),
|
||||
};
|
||||
|
||||
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
|
||||
fromDate: jest.fn(),
|
||||
toDate: jest.fn(),
|
||||
day: jest.fn(),
|
||||
time: jest.fn(),
|
||||
};
|
||||
|
||||
describe('create-ad.service', () => {
|
||||
let createAdService: CreateAdService;
|
||||
|
||||
|
@ -83,7 +75,7 @@ describe('create-ad.service', () => {
|
|||
},
|
||||
{
|
||||
provide: INPUT_DATETIME_TRANSFORMER,
|
||||
useValue: mockInputDateTimeTransformer,
|
||||
useValue: mockInputDateTimeTransformer(),
|
||||
},
|
||||
CreateAdService,
|
||||
],
|
|
@ -0,0 +1,42 @@
|
|||
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 { punctualPassengerCreateAdProps } from './ad.fixtures';
|
||||
|
||||
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps());
|
||||
jest.spyOn(ad, 'delete');
|
||||
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => ad),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
describe('delete-ad.service', () => {
|
||||
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 trigger the delete logic and delete the ad from the repository', async () => {
|
||||
await deleteAdService.execute(new DeleteAdCommand({ id: ad.id }));
|
||||
expect(ad.delete).toHaveBeenCalled();
|
||||
expect(mockAdRepository.delete).toHaveBeenCalledWith(ad);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import { DeleteUserAdsCommand } from '@modules/ad/core/application/commands/delete-user-ads/delete-user-ads.command';
|
||||
import { DeleteUserAdsService } from '@modules/ad/core/application/commands/delete-user-ads/delete-user-ads.service';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { punctualPassengerCreateAdProps } from './ad.fixtures';
|
||||
|
||||
const userAds = [
|
||||
AdEntity.create(punctualPassengerCreateAdProps()),
|
||||
AdEntity.create(punctualPassengerCreateAdProps()),
|
||||
];
|
||||
|
||||
const mockQueryBus = {
|
||||
execute: jest.fn().mockImplementation(() => userAds),
|
||||
};
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
describe('delete-user-ads.service', () => {
|
||||
let deleteUserAdsService: DeleteUserAdsService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: QueryBus,
|
||||
useValue: mockQueryBus,
|
||||
},
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
DeleteUserAdsService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteUserAdsService =
|
||||
module.get<DeleteUserAdsService>(DeleteUserAdsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteUserAdsService).toBeDefined();
|
||||
});
|
||||
|
||||
it('should call the delete command for each ad returned by the query', async () => {
|
||||
await deleteUserAdsService.execute(
|
||||
new DeleteUserAdsCommand({ id: userAds[0].getProps().userId }),
|
||||
);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(userAds.length);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
|
||||
import { FindAdByIdQueryHandler } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { punctualPassengerCreateAdProps } from './ad.fixtures';
|
||||
|
||||
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps());
|
||||
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => ad),
|
||||
};
|
||||
|
||||
describe('find-ad-by-id.query-handler', () => {
|
||||
let findAdByIdQueryHandler: FindAdByIdQueryHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
FindAdByIdQueryHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAdByIdQueryHandler = module.get<FindAdByIdQueryHandler>(
|
||||
FindAdByIdQueryHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAdByIdQueryHandler).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should return an ad', async () => {
|
||||
const findAdbyIdQuery = new FindAdByIdQuery(
|
||||
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
|
||||
);
|
||||
const ad: AdEntity =
|
||||
await findAdByIdQueryHandler.execute(findAdbyIdQuery);
|
||||
expect(ad.getProps().fromDate).toBe('2023-06-22');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
|
||||
import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const originWaypointProps: WaypointProps = {
|
||||
position: 0,
|
||||
address: {
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
},
|
||||
},
|
||||
};
|
||||
const destinationWaypointProps: WaypointProps = {
|
||||
position: 1,
|
||||
address: {
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
},
|
||||
},
|
||||
};
|
||||
const baseCreateAdProps = {
|
||||
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originWaypointProps, destinationWaypointProps],
|
||||
};
|
||||
const punctualCreateAdProps = {
|
||||
fromDate: '2023-06-22',
|
||||
toDate: '2023-06-22',
|
||||
schedule: [
|
||||
{
|
||||
day: 4,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
};
|
||||
const punctualPassengerCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
};
|
||||
|
||||
const ads: AdEntity[] = [
|
||||
AdEntity.create(punctualPassengerCreateAdProps),
|
||||
AdEntity.create(punctualPassengerCreateAdProps),
|
||||
AdEntity.create(punctualPassengerCreateAdProps),
|
||||
];
|
||||
|
||||
const mockAdRepository = {
|
||||
findAllByIds: jest.fn().mockImplementation(() => ads),
|
||||
};
|
||||
|
||||
describe('Find Ads By Ids Query Handler', () => {
|
||||
let findAdsByIdsQueryHandler: FindAdsByIdsQueryHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
FindAdsByIdsQueryHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAdsByIdsQueryHandler = module.get<FindAdsByIdsQueryHandler>(
|
||||
FindAdsByIdsQueryHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAdsByIdsQueryHandler).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should return an ad', async () => {
|
||||
const findAdsByIdsQuery = new FindAdsByIdsQuery([
|
||||
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
|
||||
'dd264806-13b4-4226-9b18-87adf0ad5dd2',
|
||||
'dd264806-13b4-4226-9b18-87adf0ad5dd3',
|
||||
]);
|
||||
const ads: AdEntity[] =
|
||||
await findAdsByIdsQueryHandler.execute(findAdsByIdsQuery);
|
||||
expect(ads).toHaveLength(3);
|
||||
expect(ads[1].getProps().fromDate).toBe('2023-06-22');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query';
|
||||
import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const originWaypointProps: WaypointProps = {
|
||||
position: 0,
|
||||
address: {
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
},
|
||||
},
|
||||
};
|
||||
const destinationWaypointProps: WaypointProps = {
|
||||
position: 1,
|
||||
address: {
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
},
|
||||
},
|
||||
};
|
||||
const baseCreateAdProps = {
|
||||
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originWaypointProps, destinationWaypointProps],
|
||||
};
|
||||
const punctualCreateAdProps = {
|
||||
fromDate: '2023-06-22',
|
||||
toDate: '2023-06-22',
|
||||
schedule: [
|
||||
{
|
||||
day: 4,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
};
|
||||
const punctualPassengerCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
};
|
||||
|
||||
const ads: AdEntity[] = [
|
||||
AdEntity.create(punctualPassengerCreateAdProps),
|
||||
AdEntity.create(punctualPassengerCreateAdProps),
|
||||
AdEntity.create(punctualPassengerCreateAdProps),
|
||||
];
|
||||
|
||||
const mockAdRepository = {
|
||||
findAll: jest.fn().mockImplementation(() => ads),
|
||||
};
|
||||
|
||||
describe('Find Ads By User Id Query Handler', () => {
|
||||
let findAdsByUserIdQueryHandler: FindAdsByUserIdQueryHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
FindAdsByUserIdQueryHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAdsByUserIdQueryHandler = module.get<FindAdsByUserIdQueryHandler>(
|
||||
FindAdsByUserIdQueryHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAdsByUserIdQueryHandler).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should return an ad', async () => {
|
||||
const findAdsByIdsQuery = new FindAdsByUserIdQuery(
|
||||
'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
);
|
||||
const ads: AdEntity[] =
|
||||
await findAdsByUserIdQueryHandler.execute(findAdsByIdsQuery);
|
||||
expect(ads).toHaveLength(3);
|
||||
expect(ads[1].getProps().fromDate).toBe('2023-06-22');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
|
||||
import { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const originWaypointProps: WaypointProps = {
|
||||
position: 0,
|
||||
address: {
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
},
|
||||
},
|
||||
};
|
||||
const destinationWaypointProps: WaypointProps = {
|
||||
position: 1,
|
||||
address: {
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
coordinates: {
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
},
|
||||
},
|
||||
};
|
||||
const baseCreateAdProps = {
|
||||
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
strict: false,
|
||||
waypoints: [originWaypointProps, destinationWaypointProps],
|
||||
};
|
||||
const punctualCreateAdProps = {
|
||||
fromDate: '2023-06-22',
|
||||
toDate: '2023-06-22',
|
||||
schedule: [
|
||||
{
|
||||
day: 4,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
};
|
||||
const punctualPassengerCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
};
|
||||
|
||||
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
|
||||
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => ad),
|
||||
update: jest.fn().mockImplementation(() => ad.id),
|
||||
};
|
||||
|
||||
describe('Invalidate Ad Service', () => {
|
||||
let invalidateAdService: InvalidateAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
InvalidateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
invalidateAdService = module.get<InvalidateAdService>(InvalidateAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(invalidateAdService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should invalidate an ad', async () => {
|
||||
jest.spyOn(ad, 'invalid');
|
||||
const invalidateAdCommand = new InvalidateAdCommand(ad.id);
|
||||
const result: AggregateID =
|
||||
await invalidateAdService.execute(invalidateAdCommand);
|
||||
expect(result).toBe(ad.id);
|
||||
expect(ad.invalid).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
|
||||
import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-events';
|
||||
import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-event';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
|
||||
import { AD_CREATED_ROUTING_KEY } from '@src/app.constants';
|
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
OUTPUT_DATETIME_TRANSFORMER,
|
||||
TIMEZONE_FINDER,
|
||||
TIME_CONVERTER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { AdUpdatedDomainEvent } from '@modules/ad/core/domain/events/ad.domain-event';
|
||||
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
|
||||
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
|
||||
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { punctualPassengerCreateAdProps } from './ad.fixtures';
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Publish message when ad is updated domain event handler', () => {
|
||||
let updatedDomainEventHandler: PublishMessageWhenAdIsUpdatedDomainEventHandler;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
{
|
||||
provide: OUTPUT_DATETIME_TRANSFORMER,
|
||||
useClass: OutputDateTimeTransformer,
|
||||
},
|
||||
{
|
||||
provide: TIMEZONE_FINDER,
|
||||
useClass: TimezoneFinder,
|
||||
},
|
||||
{
|
||||
provide: TIME_CONVERTER,
|
||||
useClass: TimeConverter,
|
||||
},
|
||||
AdMapper,
|
||||
PublishMessageWhenAdIsUpdatedDomainEventHandler,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updatedDomainEventHandler =
|
||||
module.get<PublishMessageWhenAdIsUpdatedDomainEventHandler>(
|
||||
PublishMessageWhenAdIsUpdatedDomainEventHandler,
|
||||
);
|
||||
});
|
||||
|
||||
it('should publish a message', () => {
|
||||
expect(updatedDomainEventHandler).toBeDefined();
|
||||
const ad = AdEntity.create(punctualPassengerCreateAdProps());
|
||||
const adUpdatedDomainEvent = new AdUpdatedDomainEvent(ad);
|
||||
updatedDomainEventHandler.handle(adUpdatedDomainEvent);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
AD_REPOSITORY,
|
||||
INPUT_DATETIME_TRANSFORMER,
|
||||
} 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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { Status } from '@modules/ad/core/domain/ad.types';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { mockInputDateTimeTransformer } from '../ad.mocks';
|
||||
import { punctualCreateAdRequest } from '../interface/ad.fixtures';
|
||||
import { punctualPassengerCreateAdProps } from './ad.fixtures';
|
||||
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(
|
||||
async (id) =>
|
||||
new AdEntity({
|
||||
id,
|
||||
props: { ...punctualPassengerCreateAdProps(), status: Status.VALID },
|
||||
}),
|
||||
),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockEventEmitter = {
|
||||
emitAsync: jest.fn(),
|
||||
};
|
||||
|
||||
describe('create-ad.service', () => {
|
||||
let updateAdService: UpdateAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
{
|
||||
provide: INPUT_DATETIME_TRANSFORMER,
|
||||
useValue: mockInputDateTimeTransformer(),
|
||||
},
|
||||
{
|
||||
provide: EventEmitter2,
|
||||
useValue: mockEventEmitter,
|
||||
},
|
||||
UpdateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateAdService = module.get<UpdateAdService>(UpdateAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateAdService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should update the ad in the repository and emit an event', async () => {
|
||||
const command = new UpdateAdCommand({
|
||||
adId: '200d61a8-d878-4378-a609-c19ea71633d2',
|
||||
...punctualCreateAdRequest(),
|
||||
});
|
||||
|
||||
await updateAdService.execute(command);
|
||||
expect(mockAdRepository.update).toHaveBeenCalled();
|
||||
expect(mockEventEmitter.emitAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,8 +1,9 @@
|
|||
import { AggregateID } from '@mobicoop/ddd-library';
|
||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
|
||||
import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
|
||||
import { FindAdByIdQueryHandler } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
|
@ -44,7 +45,9 @@ const punctualCreateAdProps = {
|
|||
toDate: '2023-06-22',
|
||||
schedule: [
|
||||
{
|
||||
day: 4,
|
||||
time: '08:30',
|
||||
margin: 900,
|
||||
},
|
||||
],
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
|
@ -60,10 +63,11 @@ const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
|
|||
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => ad),
|
||||
update: jest.fn().mockImplementation(() => ad.id),
|
||||
};
|
||||
|
||||
describe('find-ad-by-id.query-handler', () => {
|
||||
let findAdByIdQueryHandler: FindAdByIdQueryHandler;
|
||||
describe('Validate Ad Service', () => {
|
||||
let validateAdService: ValidateAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
|
@ -72,27 +76,25 @@ describe('find-ad-by-id.query-handler', () => {
|
|||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
FindAdByIdQueryHandler,
|
||||
ValidateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAdByIdQueryHandler = module.get<FindAdByIdQueryHandler>(
|
||||
FindAdByIdQueryHandler,
|
||||
);
|
||||
validateAdService = module.get<ValidateAdService>(ValidateAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAdByIdQueryHandler).toBeDefined();
|
||||
expect(validateAdService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should return an ad', async () => {
|
||||
const findAdbyIdQuery = new FindAdByIdQuery(
|
||||
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
|
||||
);
|
||||
const ad: AdEntity =
|
||||
await findAdByIdQueryHandler.execute(findAdbyIdQuery);
|
||||
expect(ad.getProps().fromDate).toBe('2023-06-22');
|
||||
it('should validate an ad', async () => {
|
||||
jest.spyOn(ad, 'valid');
|
||||
const validateAdCommand = new ValidateAdCommand(ad.id);
|
||||
const result: AggregateID =
|
||||
await validateAdService.execute(validateAdCommand);
|
||||
expect(result).toBe(ad.id);
|
||||
expect(ad.valid).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
|
||||
const originWaypoint: WaypointDto = {
|
||||
position: 0,
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
};
|
||||
const destinationWaypoint: WaypointDto = {
|
||||
position: 1,
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
};
|
||||
export function punctualCreateAdRequest(): CreateAdRequestDto {
|
||||
return {
|
||||
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
|
||||
fromDate: '2023-12-21',
|
||||
toDate: '2023-12-21',
|
||||
schedule: [
|
||||
{
|
||||
time: '08:15',
|
||||
day: 4,
|
||||
margin: 600,
|
||||
},
|
||||
],
|
||||
driver: false,
|
||||
passenger: true,
|
||||
seatsRequested: 1,
|
||||
seatsProposed: 3,
|
||||
strict: false,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
};
|
||||
}
|
|
@ -1,51 +1,10 @@
|
|||
import { IdResponse } from '@mobicoop/ddd-library';
|
||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { IdResponse, RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller';
|
||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
|
||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const originWaypoint: WaypointDto = {
|
||||
position: 0,
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
};
|
||||
const destinationWaypoint: WaypointDto = {
|
||||
position: 1,
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
};
|
||||
const punctualCreateAdRequest: CreateAdRequestDto = {
|
||||
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
|
||||
fromDate: '2023-12-21',
|
||||
toDate: '2023-12-21',
|
||||
schedule: [
|
||||
{
|
||||
time: '08:15',
|
||||
day: 4,
|
||||
margin: 600,
|
||||
},
|
||||
],
|
||||
driver: false,
|
||||
passenger: true,
|
||||
seatsRequested: 1,
|
||||
seatsProposed: 3,
|
||||
strict: false,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
};
|
||||
import { punctualCreateAdRequest } from './ad.fixtures';
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest
|
||||
|
@ -89,7 +48,7 @@ describe('Create Ad Grpc Controller', () => {
|
|||
it('should create a new ad', async () => {
|
||||
jest.spyOn(mockCommandBus, 'execute');
|
||||
const result: IdResponse = await createAdGrpcController.create(
|
||||
punctualCreateAdRequest,
|
||||
punctualCreateAdRequest(),
|
||||
);
|
||||
expect(result).toBeInstanceOf(IdResponse);
|
||||
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
|
||||
|
@ -100,7 +59,7 @@ describe('Create Ad Grpc Controller', () => {
|
|||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createAdGrpcController.create(punctualCreateAdRequest);
|
||||
await createAdGrpcController.create(punctualCreateAdRequest());
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||
|
@ -112,7 +71,7 @@ describe('Create Ad Grpc Controller', () => {
|
|||
jest.spyOn(mockCommandBus, 'execute');
|
||||
expect.assertions(3);
|
||||
try {
|
||||
await createAdGrpcController.create(punctualCreateAdRequest);
|
||||
await createAdGrpcController.create(punctualCreateAdRequest());
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
|
@ -0,0 +1,42 @@
|
|||
import { DeleteAdGrpcController } from '@modules/ad/interface/grpc-controllers/delete-ad.grpc.controller';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Delete Ad Grpc Controller', () => {
|
||||
let deleteAdGrpcController: DeleteAdGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
DeleteAdGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
deleteAdGrpcController = module.get<DeleteAdGrpcController>(
|
||||
DeleteAdGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(deleteAdGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should execute the delete ad command', async () => {
|
||||
await deleteAdGrpcController.delete({
|
||||
id: '200d61a8-d878-4378-a609-c19ea71633d2',
|
||||
});
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,132 @@
|
|||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { FindAdsByIdsGrpcController } from '@modules/ad/interface/grpc-controllers/find-ads-by-ids.grpc.controller';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockQueryBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => [
|
||||
'200d61a8-d878-4378-a609-c19ea71633d2',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d3',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d4',
|
||||
])
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
const mockAdMapper = {
|
||||
toResponse: jest.fn().mockImplementationOnce(() => ({
|
||||
userId: '8cc90d1a-4a59-4289-a7d8-078f9db7857f',
|
||||
driver: true,
|
||||
passenger: true,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
fromDate: '2023-06-27',
|
||||
toDate: '2023-06-27',
|
||||
schedule: {
|
||||
tue: '07:15',
|
||||
},
|
||||
marginDurations: {
|
||||
mon: 900,
|
||||
tue: 900,
|
||||
wed: 900,
|
||||
thu: 900,
|
||||
fri: 900,
|
||||
sat: 900,
|
||||
sun: 900,
|
||||
},
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
waypoints: [
|
||||
{
|
||||
position: 0,
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
},
|
||||
{
|
||||
position: 1,
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
describe('Find Ads By Ids Grpc Controller', () => {
|
||||
let findAdsByIdsGrpcController: FindAdsByIdsGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: QueryBus,
|
||||
useValue: mockQueryBus,
|
||||
},
|
||||
{
|
||||
provide: AdMapper,
|
||||
useValue: mockAdMapper,
|
||||
},
|
||||
FindAdsByIdsGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAdsByIdsGrpcController = module.get<FindAdsByIdsGrpcController>(
|
||||
FindAdsByIdsGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAdsByIdsGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return ads', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockAdMapper, 'toResponse');
|
||||
const response = await findAdsByIdsGrpcController.findAllByIds({
|
||||
ids: [
|
||||
'200d61a8-d878-4378-a609-c19ea71633d2',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d3',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d4',
|
||||
],
|
||||
});
|
||||
expect(response.ads).toHaveLength(3);
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockAdMapper, 'toResponse');
|
||||
expect.assertions(4);
|
||||
try {
|
||||
await findAdsByIdsGrpcController.findAllByIds({
|
||||
ids: [
|
||||
'200d61a8-d878-4378-a609-c19ea71633d2',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d3',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d4',
|
||||
],
|
||||
});
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||
import { FindAdsByUserIdGrpcController } from '@modules/ad/interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { RpcException } from '@nestjs/microservices';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockQueryBus = {
|
||||
execute: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => [
|
||||
'200d61a8-d878-4378-a609-c19ea71633d2',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d3',
|
||||
'200d61a8-d878-4378-a609-c19ea71633d4',
|
||||
])
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
}),
|
||||
};
|
||||
|
||||
const mockAdMapper = {
|
||||
toResponse: jest.fn().mockImplementationOnce(() => ({
|
||||
userId: '8cc90d1a-4a59-4289-a7d8-078f9db7857f',
|
||||
driver: true,
|
||||
passenger: true,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
fromDate: '2023-06-27',
|
||||
toDate: '2023-06-27',
|
||||
schedule: {
|
||||
tue: '07:15',
|
||||
},
|
||||
marginDurations: {
|
||||
mon: 900,
|
||||
tue: 900,
|
||||
wed: 900,
|
||||
thu: 900,
|
||||
fri: 900,
|
||||
sat: 900,
|
||||
sun: 900,
|
||||
},
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
waypoints: [
|
||||
{
|
||||
position: 0,
|
||||
lat: 48.689445,
|
||||
lon: 6.17651,
|
||||
houseNumber: '5',
|
||||
street: 'Avenue Foch',
|
||||
locality: 'Nancy',
|
||||
postalCode: '54000',
|
||||
country: 'France',
|
||||
},
|
||||
{
|
||||
position: 1,
|
||||
lat: 48.8566,
|
||||
lon: 2.3522,
|
||||
locality: 'Paris',
|
||||
postalCode: '75000',
|
||||
country: 'France',
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
describe('Find Ads By User Id Grpc Controller', () => {
|
||||
let findAdsByUserIdGrpcController: FindAdsByUserIdGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: QueryBus,
|
||||
useValue: mockQueryBus,
|
||||
},
|
||||
{
|
||||
provide: AdMapper,
|
||||
useValue: mockAdMapper,
|
||||
},
|
||||
FindAdsByUserIdGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
findAdsByUserIdGrpcController = module.get<FindAdsByUserIdGrpcController>(
|
||||
FindAdsByUserIdGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(findAdsByUserIdGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return ads', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockAdMapper, 'toResponse');
|
||||
const response = await findAdsByUserIdGrpcController.findAllByUserId({
|
||||
id: '8cc90d1a-4a59-4289-a7d8-078f9db7857f',
|
||||
});
|
||||
expect(response.ads).toHaveLength(3);
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should throw a generic RpcException', async () => {
|
||||
jest.spyOn(mockQueryBus, 'execute');
|
||||
jest.spyOn(mockAdMapper, 'toResponse');
|
||||
expect.assertions(4);
|
||||
try {
|
||||
await findAdsByUserIdGrpcController.findAllByUserId({
|
||||
id: '8cc90d1a-4a59-4289-a7d8-078f9db7857f',
|
||||
});
|
||||
} catch (e: any) {
|
||||
expect(e).toBeInstanceOf(RpcException);
|
||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
||||
}
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
expect(mockAdMapper.toResponse).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue