40 Commits

Author SHA1 Message Date
Romain Thouvenin
a7b342c049 Add optional comment to Ad type and records #7409 2024-02-29 14:47:53 +01:00
Sylvain Briat
da4b30350b fix coverage results path in package.json 2024-02-26 15:07:42 +01:00
Sylvain Briat
55c7e2b11c removed local vscode settings 2024-02-20 16:44:28 +01:00
Sylvain Briat
c52afbb243 update readme 2024-02-16 16:52:20 +01:00
Sylvain Briat
98d2b521ab add controller in module 2024-02-16 16:52:20 +01:00
Sylvain Briat
bbb96cfd36 find ads by user id query, dto and controller 2024-02-16 16:52:14 +01:00
Sylvain Briat
909ef04e69 move tests folder to the root 2024-02-16 16:02:04 +01:00
Fanch
540c63d297 copy file from v3 gitlab template repo 2024-02-05 19:16:00 +01:00
Sylvain Briat
c72c64e6da Merge branch 'use_prisma_deploy' into 'main'
use prisma deploy as default for migrate, add migrate:dev command

See merge request v3/service/ad!37
2024-01-31 14:18:11 +00:00
Fanch
8f57dc2c7a use prisma deploy as default for migrate, add migrate:dev command 2024-01-31 12:47:27 +01:00
Fanch
41073539bf Merge branch 'fix_test_install' into 'main'
Fix test install

See merge request v3/service/ad!34
2024-01-24 14:15:51 +00:00
Fanch
483e947d92 use node lts image for docker 2024-01-23 14:50:32 +01:00
Fanch
b13df86745 use full registry path for docker image 2024-01-23 14:50:32 +01:00
Fanch
61c1d6ffcb Merge branch 'fixAdValidation' into 'main'
Fix ad validation

See merge request v3/service/ad!36
2024-01-19 14:02:19 +00:00
Sylvain Briat
fbc0ae2a33 2.4.5 2024-01-18 15:53:57 +01:00
Sylvain Briat
b039dbb3bd fix ad validation : remove schedule and waypoints duplication 2024-01-18 15:53:48 +01:00
Sylvain Briat
2009355b18 2.4.4 2024-01-17 12:00:00 +01:00
Sylvain Briat
5d6547a184 fix bad validation : wrong cascade update after ad status change 2024-01-17 11:59:51 +01:00
Sylvain Briat
40e8b5f733 Merge branch 'updatePackages' into 'main'
Update packages

See merge request v3/service/ad!35
2024-01-17 08:03:01 +00:00
Sylvain Briat
98068d021f pretty 2024-01-17 08:56:34 +01:00
Sylvain Briat
4e236551ae 2.4.3 2024-01-17 08:51:40 +01:00
Sylvain Briat
4bd7ca64de update packages 2024-01-17 08:51:34 +01:00
Sylvain Briat
3e1c4afce3 Merge branch 'secureBroker' into 'main'
Secure broker

See merge request v3/service/ad!33
2023-12-18 14:36:48 +00:00
Sylvain Briat
d4a37b237e 2.4.2 2023-12-18 15:32:39 +01:00
Sylvain Briat
b2cf66139a secure broker 2023-12-18 15:31:39 +01:00
Sylvain Briat
99017b0e55 Merge branch 'fixAdValidation' into 'main'
Fix ad validation

See merge request v3/service/ad!32
2023-12-07 10:16:04 +00:00
Sylvain Briat
ee0a2cb386 2.4.1 2023-12-07 11:11:59 +01:00
Sylvain Briat
f69e8a95f1 fix ad validation after matcher ad creation 2023-12-07 11:11:52 +01:00
Sylvain Briat
976a3c3779 Merge branch 'adStatus' into 'main'
Handle matcher messages, handle ad statuses

See merge request v3/service/ad!31
2023-12-06 15:13:02 +00:00
Sylvain Briat
c85d6fb756 2.4.0 2023-12-06 15:51:50 +01:00
Sylvain Briat
e0a4b07733 handle matcher messages 2023-12-06 15:51:46 +01:00
Sylvain Briat
dfe4db8276 Merge branch 'findAllByIds' into 'main'
Find all by ids

See merge request v3/service/ad!30
2023-11-22 16:23:49 +00:00
Sylvain Briat
88a975a8a1 2.3.0 2023-11-22 17:19:58 +01:00
Sylvain Briat
263133ec30 find all ads by ids 2023-11-22 17:19:53 +01:00
Sylvain Briat
3d29eb4517 Merge branch 'security' into 'main'
Improve security : add sast and secret detection in gitlab ci

See merge request v3/service/ad!29
2023-11-06 07:53:07 +00:00
Sylvain Briat
d3c305dbce Improve security : add sast and secret detection in gitlab ci 2023-11-06 08:48:56 +01:00
Sylvain Briat
4844f07e08 Merge branch 'updatePackages' into 'main'
Update packages

See merge request v3/service/ad!28
2023-10-31 09:05:51 +00:00
Sylvain Briat
5dd02aa0d2 2.2.5 2023-10-31 10:02:54 +01:00
Sylvain Briat
a3c503af7e update packages 2023-10-31 10:01:57 +01:00
Sylvain Briat
66b831d5d2 Merge branch 'removeConfigurationPackageV3' into 'main'
Remove configuration package v3

See merge request v3/service/ad!27
2023-10-30 15:28:25 +00:00
79 changed files with 2804 additions and 1226 deletions

View File

@@ -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

View File

@@ -4,6 +4,10 @@ stages:
- test
- build
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
##############
# TEST STAGE #
##############

View File

@@ -0,0 +1,55 @@
_Replace italic text by your own description_
## Feature Merge Request
### Why this Merge Request
_This merge request addresses, and describe the problem or user story being addressed._
### What is implemented, what is the chosen solution
_Explain the fix or solution implemented. Which other solution have been envisaged._
### Related issues and impact on other project in codebase
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
_And Link to other project Impacted._
### Other Information
_Include any extra information or considerations for reviewers._
## Checklists
### Merge Request
- [ ] Target branch identified.
- [ ] Code based on last version of target branch.
- [ ] Description filled.
- [ ] Impact on other project codebase identified.
- [ ] Documentation reflects the changes made.
- [ ] Test run in gitlab pipeline and locally.
- [ ] One or more reviewer is defined
### Code Review
- [ ] Code follows project coding guidelines.
- [ ] Code follows project designed architecture.
- [ ] Code is easily readable.
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
- [ ] No redundant/duplicate code (unless explain by architecture choice)
- [ ] Commit are all related to MR and well written (Atomic commit).
- [ ] New code is tested and covered by automated test.
- [ ] No useless logging or debugging code.
- [ ] No code can be replaced by library or framework code.
### TODO before merge
- [ ] _add any task here_
- [ ] ...
### TODO after merge
- [ ] _add any task here_
- [ ] ...

View File

@@ -0,0 +1,62 @@
_Replace italic text by your own description_
## Release Merge Request
### Why this Merge Request
_This merge request addresses, and describe the problem or user story being addressed._
### What is implemented, what is the chosen solution
_Explain the fix or solution implemented. Which other solution have been envisaged._
### Related issues and impact on other project in codebase
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
_And Link to other project Impacted._
### Other Information
_Include any extra information or considerations for reviewers._
## Checklists
### Merge Request
- [ ] Target branch identified.
- [ ] Code based on last version of target branch.
- [ ] Description filled.
- [ ] Impact on other project codebase identified.
- [ ] Documentation reflects the changes made.
- [ ] Test run in gitlab pipeline and locally.
- [ ] One or more reviewer is defined
### Code Review
- [ ] Code follows project coding guidelines.
- [ ] Code follows project designed architecture.
- [ ] Code is easily readable.
- [ ] Everything new have an explicit and pertinent name (variable, method, file ...)
- [ ] No redundant/duplicate code (unless explain by architecture choice)
- [ ] Commit are all related to MR and well written (Atomic commit).
- [ ] New code is tested and covered by automated test.
- [ ] No useless logging or debugging code.
- [ ] No code can be replaced by library or framework code.
### Change Management
- [ ] Release is planned
- [ ] Merge Request to be included are identified
- [ ] Concerned Team are aware of the change
- [ ] No other change on the same day (if possible)
### TODO before merge
- [ ] _add any task here_
- [ ] ...
### TODO after merge
- [ ] _add any task here_
- [ ] ...

View File

@@ -0,0 +1,37 @@
_Replace italic text by your own description_
## Small Fix Merge Request
### Why this Merge Request
_This merge request addresses, and describe the problem or user story being addressed._
### What is implemented, what is the chosen solution
_Explain the fix or solution implemented. Which other solution have been envisaged._
### Related issues and impact on other project in codebase
_Provide links to the related issues, feature requests and merge request (from Gitlab and Redmine)._
_And Link to other project Impacted._
### Other Information
_Include any extra information or considerations for reviewers._
## Checklists
### Merge Request
- [ ] Target branch identified.
- [ ] Code based on last version of target branch.
- [ ] Description filled.
- [ ] Impact on other project codebase identified.
- [ ] Test run in gitlab pipeline and locally.
### Code Review
- [ ] Code is easily readable.
- [ ] Commit are all related to MR and well written (Atomic commit).
- [ ] No useless logging or debugging code.

View File

@@ -4,3 +4,4 @@ node_modules
dist
coverage
.prettierrc.json
.gitlab

View File

@@ -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 ./

View File

@@ -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,6 +230,7 @@ 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
## Messages

View File

@@ -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

View File

@@ -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

2328
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@mobicoop/ad",
"version": "2.2.4",
"version": "2.4.5",
"description": "Mobicoop V3 Ad",
"author": "sbriat",
"private": true,
@@ -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.4.3",
"@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"
}

View File

@@ -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';

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ad" ADD COLUMN "comment" TEXT;

View File

@@ -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
}

View File

@@ -5,9 +5,19 @@ 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';
// 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';
// configuration
export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set';
export const SERVICE_CONFIGURATION_DELETE_QUEUE = 'ad-configuration-delete';

View File

@@ -8,7 +8,7 @@ import {
WaypointModel,
ScheduleItemModel,
} from './infrastructure/ad.repository';
import { Frequency } from './core/domain/ad.types';
import { Frequency, Status } 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';
@@ -39,46 +39,50 @@ export class AdMapper
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) => ({
uuid: v4(),
day: scheduleItem.day as number,
time: new Date(
1970,
0,
1,
parseInt(scheduleItem.time.split(':')[0]),
parseInt(scheduleItem.time.split(':')[1]),
),
margin: scheduleItem.margin as number,
createdAt: now,
updatedAt: now,
})),
},
schedule: copy.schedule
? {
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
uuid: v4(),
day: scheduleItem.day as number,
time: new Date(
1970,
0,
1,
parseInt(scheduleItem.time.split(':')[0]),
parseInt(scheduleItem.time.split(':')[1]),
),
margin: scheduleItem.margin as number,
createdAt: now,
updatedAt: now,
})),
}
: undefined,
seatsProposed: copy.seatsProposed as number,
seatsRequested: copy.seatsRequested as number,
strict: copy.strict as boolean,
waypoints: {
create: copy.waypoints.map((waypoint: WaypointProps) => ({
uuid: v4(),
position: waypoint.position,
name: waypoint.address.name,
houseNumber: waypoint.address.houseNumber,
street: waypoint.address.street,
locality: waypoint.address.locality,
postalCode: waypoint.address.postalCode,
country: waypoint.address.country,
lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat,
createdAt: now,
updatedAt: now,
})),
},
createdAt: copy.createdAt,
updatedAt: copy.updatedAt,
waypoints: copy.waypoints
? {
create: copy.waypoints.map((waypoint: WaypointProps) => ({
uuid: v4(),
position: waypoint.position,
name: waypoint.address.name,
houseNumber: waypoint.address.houseNumber,
street: waypoint.address.street,
locality: waypoint.address.locality,
postalCode: waypoint.address.postalCode,
country: waypoint.address.country,
lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat,
createdAt: now,
updatedAt: now,
})),
}
: undefined,
comment: copy.comment,
};
return record;
};
@@ -92,10 +96,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 +114,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 +129,7 @@ export class AdMapper
},
},
})),
comment: record.comment,
},
});
return entity;
@@ -135,6 +141,7 @@ export class AdMapper
response.userId = props.userId;
response.driver = props.driver as boolean;
response.passenger = props.passenger as boolean;
response.status = props.status;
response.frequency = props.frequency;
response.fromDate = this.outputDatetimeTransformer.fromDate(
{
@@ -188,6 +195,7 @@ export class AdMapper
lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat,
}));
response.comment = props.comment;
return response;
};
}

View File

@@ -21,16 +21,42 @@ import { PrismaService } from './infrastructure/prisma.service';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller';
import { FindAdsByIdsQueryHandler } from './core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler';
import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service';
import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler';
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
import { FindAdsByUserIdQueryHandler } from './core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
const grpcControllers = [
CreateAdGrpcController,
FindAdByIdGrpcController,
FindAdsByIdsGrpcController,
FindAdsByUserIdGrpcController,
];
const messageHandlers = [
MatcherAdCreatedMessageHandler,
MatcherAdCreationFailedMessageHandler,
];
const eventHandlers: Provider[] = [
PublishMessageWhenAdIsCreatedDomainEventHandler,
];
const commandHandlers: Provider[] = [CreateAdService];
const commandHandlers: Provider[] = [
CreateAdService,
ValidateAdService,
InvalidateAdService,
];
const queryHandlers: Provider[] = [FindAdByIdQueryHandler];
const queryHandlers: Provider[] = [
FindAdByIdQueryHandler,
FindAdsByIdsQueryHandler,
FindAdsByUserIdQueryHandler,
];
const mappers: Provider[] = [AdMapper];
@@ -72,6 +98,7 @@ const adapters: Provider[] = [
imports: [CqrsModule],
controllers: [...grpcControllers],
providers: [
...messageHandlers,
...eventHandlers,
...commandHandlers,
...queryHandlers,

View File

@@ -15,6 +15,7 @@ export class CreateAdCommand extends Command {
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;
}
}

View File

@@ -95,6 +95,7 @@ export class CreateAdService implements ICommandHandler {
},
},
})),
comment: command.comment,
});
try {

View File

@@ -0,0 +1,7 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class InvalidateAdCommand extends Command {
constructor(props: CommandProps<InvalidateAdCommand>) {
super(props);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class ValidateAdCommand extends Command {
constructor(props: CommandProps<ValidateAdCommand>) {
super(props);
}
}

View File

@@ -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;
}
}

View File

@@ -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';

View File

@@ -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,
});
}
}

View File

@@ -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;
}
}

View File

@@ -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,
},
);
}
}

View File

@@ -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;
}
}

View File

@@ -1,19 +1,26 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid';
import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
import { AdProps, CreateAdProps } from './ad.types';
import { AdCreatedDomainEvent } from './events/ad-created.domain-event';
import { AdProps, CreateAdProps, Status } from './ad.types';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { WaypointProps } from './value-objects/waypoint.value-object';
import { AdValidatedDomainEvent } from './events/ad-validated.domain-event';
import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event';
import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event';
export class AdEntity extends AggregateRoot<AdProps> {
protected readonly _id: AggregateID;
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,
@@ -40,11 +47,54 @@ 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;
};
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}

View File

@@ -5,6 +5,7 @@ import { WaypointProps } from './value-objects/waypoint.value-object';
export interface AdProps {
userId: string;
driver: boolean;
status: Status;
passenger: boolean;
frequency: Frequency;
fromDate: string;
@@ -14,6 +15,7 @@ export interface AdProps {
seatsRequested: number;
strict: boolean;
waypoints: WaypointProps[];
comment?: string;
}
// Properties that are needed for an Ad creation
@@ -29,9 +31,17 @@ export interface CreateAdProps {
seatsRequested: number;
strict: boolean;
waypoints: WaypointProps[];
comment?: string;
}
export enum Frequency {
PUNCTUAL = 'PUNCTUAL',
RECURRENT = 'RECURRENT',
}
export enum Status {
PENDING = 'PENDING',
VALID = 'VALID',
INVALID = 'INVALID',
SUSPENDED = 'SUSPENDED',
}

View File

@@ -12,6 +12,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
readonly seatsRequested: number;
readonly strict: boolean;
readonly waypoints: Waypoint[];
readonly comment?: string;
constructor(props: DomainEventProps<AdCreatedDomainEvent>) {
super(props);
@@ -26,6 +27,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
this.seatsRequested = props.seatsRequested;
this.strict = props.strict;
this.waypoints = props.waypoints;
this.comment = props.comment;
}
}

View File

@@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AdInvalidatedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AdInvalidatedDomainEvent>) {
super(props);
}
}

View File

@@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AdSuspendedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AdSuspendedDomainEvent>) {
super(props);
}
}

View File

@@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AdValidatedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AdValidatedDomainEvent>) {
super(props);
}
}

View File

@@ -17,28 +17,34 @@ export type AdBaseModel = {
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: {
create: ScheduleItemModel[];
};
schedule: ScheduleWriteModel | undefined;
waypoints: WaypointWriteModel | undefined;
};
export type ScheduleWriteModel = {
create: ScheduleItemModel[];
};
export type WaypointWriteModel = {
create: WaypointModel[];
};
export type ScheduleItemModel = {

View File

@@ -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;
}

View File

@@ -0,0 +1,5 @@
import { AdResponseDto } from './ad.response.dto';
export class AdsResponseDto {
readonly ads: readonly AdResponseDto[];
}

View File

@@ -4,7 +4,8 @@ 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 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 {
@@ -52,14 +62,8 @@ enum Frequency {
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 {}

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
import { ArrayMinSize, IsArray } from 'class-validator';
export class FindAdsByIdsRequestDto {
@IsArray()
@ArrayMinSize(1)
ids: string[];
}

View File

@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class FindAdsByUserIdRequestDto {
@IsString()
id: string;
}

View File

@@ -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,
});
}
}
}

View File

@@ -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,
});
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,4 @@
import { IntegrationEvent } from '@mobicoop/ddd-library';
export type MatcherAdCreatedIntegrationEvent = IntegrationEvent;
export type MatcherAdCreationFailedIntegrationEvent = IntegrationEvent;

View File

@@ -2,7 +2,15 @@ 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 {
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,
} from '@src/app.constants';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
@@ -24,6 +32,16 @@ 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,
},
},
}),
}),
];

View File

@@ -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 () => {

View File

@@ -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);
});
});

View File

@@ -0,0 +1,105 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
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';
import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
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: [
{
time: '08:30',
},
],
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');
});
});
});

View File

@@ -0,0 +1,103 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
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';
import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query';
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: [
{
time: '08:30',
},
],
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');
});
});
});

View File

@@ -0,0 +1,98 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AggregateID } from '@mobicoop/ddd-library';
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 { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service';
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
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: [
{
time: '08:30',
},
],
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);
});
});
});

View File

@@ -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';

View File

@@ -0,0 +1,98 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service';
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
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: [
{
time: '08:30',
},
],
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('Validate Ad Service', () => {
let validateAdService: ValidateAdService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
ValidateAdService,
],
}).compile();
validateAdService = module.get<ValidateAdService>(ValidateAdService);
});
it('should be defined', () => {
expect(validateAdService).toBeDefined();
});
describe('execution', () => {
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);
});
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -0,0 +1,46 @@
import { MatcherAdCreatedMessageHandler } from '@modules/ad/interface/message-handlers/matcher-ad-created.message-handler';
import { CommandBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const matcherAdCreatedMessage =
'{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driverDuration":"3512","driverDistance":"65845","fwdAzimuth":"90","backAzimuth":"270"}';
const mockCommandBus = {
execute: jest.fn(),
};
describe('Matcher Ad Created Message Handler', () => {
let matcherAdCreatedMessageHandler: MatcherAdCreatedMessageHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
MatcherAdCreatedMessageHandler,
],
}).compile();
matcherAdCreatedMessageHandler = module.get<MatcherAdCreatedMessageHandler>(
MatcherAdCreatedMessageHandler,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(matcherAdCreatedMessageHandler).toBeDefined();
});
it('should validate an ad', async () => {
jest.spyOn(mockCommandBus, 'execute');
await matcherAdCreatedMessageHandler.matcherAdCreated(
matcherAdCreatedMessage,
);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,47 @@
import { MatcherAdCreationFailedMessageHandler } from '@modules/ad/interface/message-handlers/matcher-ad-creation-failed.message-handler';
import { CommandBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';
const matcherAdCreationFailedMessage =
'{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4"}';
const mockCommandBus = {
execute: jest.fn(),
};
describe('Matcher Ad Creation Failed Message Handler', () => {
let matcherAdCreationFailedMessageHandler: MatcherAdCreationFailedMessageHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: CommandBus,
useValue: mockCommandBus,
},
MatcherAdCreationFailedMessageHandler,
],
}).compile();
matcherAdCreationFailedMessageHandler =
module.get<MatcherAdCreationFailedMessageHandler>(
MatcherAdCreationFailedMessageHandler,
);
});
afterEach(async () => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(matcherAdCreationFailedMessageHandler).toBeDefined();
});
it('should invalidate an ad', async () => {
jest.spyOn(mockCommandBus, 'execute');
await matcherAdCreationFailedMessageHandler.matcherAdCreationFailed(
matcherAdCreationFailedMessage,
);
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
});
});

View File

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