Compare commits
19 Commits
2.4.5
...
next-relea
Author | SHA1 | Date |
---|---|---|
|
12c237b980 | |
|
f6f9696620 | |
|
5aa4d9e568 | |
|
f6c3204708 | |
|
659c1baea8 | |
|
7a84bff260 | |
|
3d4ff00066 | |
|
3ff5277d5f | |
|
62e5fd56d9 | |
|
c7d4792893 | |
|
5e449ad69a | |
|
51ca6cf9c4 | |
|
be2af64f60 | |
|
9fb7ef2eac | |
|
492bb3ca44 | |
|
e8903099d7 | |
|
b17fc32a12 | |
|
8c7512b6c3 | |
|
15236904e3 |
|
@ -7,52 +7,7 @@ stages:
|
||||||
include:
|
include:
|
||||||
- template: Security/SAST.gitlab-ci.yml
|
- template: Security/SAST.gitlab-ci.yml
|
||||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||||
|
- project: mobicoop/v3/gitlab-templates
|
||||||
##############
|
file:
|
||||||
# TEST STAGE #
|
- /ci/service.test-job.yml
|
||||||
##############
|
- /ci/release.build-job.yml
|
||||||
|
|
||||||
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
|
|
||||||
|
|
25
README.md
25
README.md
|
@ -232,11 +232,34 @@ The app exposes the following [gRPC](https://grpc.io/) services :
|
||||||
- waypoints: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
|
- waypoints: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
|
||||||
- comment: optional freetext comment / description about the ad
|
- 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
|
## 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 :
|
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
|
## Tests / ESLint / Prettier
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,11 @@ services:
|
||||||
- .:/usr/src/app
|
- .:/usr/src/app
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: npm run start:dev
|
command: npm run start:debug
|
||||||
ports:
|
ports:
|
||||||
- ${SERVICE_PORT:-5006}:${SERVICE_PORT:-5006}
|
- ${SERVICE_PORT:-5006}:${SERVICE_PORT:-5006}
|
||||||
- ${HEALTH_SERVICE_PORT:-6006}:${HEALTH_SERVICE_PORT:-6006}
|
- ${HEALTH_SERVICE_PORT:-6006}:${HEALTH_SERVICE_PORT:-6006}
|
||||||
|
- 9226:9229
|
||||||
networks:
|
networks:
|
||||||
v3-network:
|
v3-network:
|
||||||
aliases:
|
aliases:
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/ad",
|
"name": "@mobicoop/ad",
|
||||||
"version": "2.4.5",
|
"version": "2.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@mobicoop/ad",
|
"name": "@mobicoop/ad",
|
||||||
"version": "2.4.5",
|
"version": "2.6.0",
|
||||||
"license": "AGPL",
|
"license": "AGPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.9.14",
|
"@grpc/grpc-js": "^1.9.14",
|
||||||
"@grpc/proto-loader": "^0.7.10",
|
"@grpc/proto-loader": "^0.7.10",
|
||||||
"@mobicoop/ddd-library": "^2.4.3",
|
"@mobicoop/ddd-library": "^2.5.0",
|
||||||
"@mobicoop/health-module": "^2.3.2",
|
"@mobicoop/health-module": "^2.3.2",
|
||||||
"@mobicoop/message-broker-module": "^2.1.2",
|
"@mobicoop/message-broker-module": "^2.1.2",
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.0",
|
||||||
|
@ -1852,9 +1852,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mobicoop/ddd-library": {
|
"node_modules/@mobicoop/ddd-library": {
|
||||||
"version": "2.4.3",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.5.0.tgz",
|
||||||
"integrity": "sha512-HxNtAfov8ne7XsFTSIDI811r3L1VDV9YUikgX7HPjrB8u2gQh6FQFnIz3Fjb/zWOGxrDEIy8HEM0AYmXkf8ULA==",
|
"integrity": "sha512-dTx7KTILs53HCqNx0BDVTzIZxfPW3pi0fZ4UMw/vDNm3oTqGA+jg7YBfNxn8yadM+j2dDIN5Kum43CmKGH8yYA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/event-emitter": "^2.0.3",
|
"@nestjs/event-emitter": "^2.0.3",
|
||||||
"@nestjs/microservices": "^10.3.0",
|
"@nestjs/microservices": "^10.3.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/ad",
|
"name": "@mobicoop/ad",
|
||||||
"version": "2.4.5",
|
"version": "2.6.0",
|
||||||
"description": "Mobicoop V3 Ad",
|
"description": "Mobicoop V3 Ad",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
"@grpc/grpc-js": "^1.9.14",
|
"@grpc/grpc-js": "^1.9.14",
|
||||||
"@grpc/proto-loader": "^0.7.10",
|
"@grpc/proto-loader": "^0.7.10",
|
||||||
"@songkeys/nestjs-redis": "^10.0.0",
|
"@songkeys/nestjs-redis": "^10.0.0",
|
||||||
"@mobicoop/ddd-library": "^2.4.3",
|
"@mobicoop/ddd-library": "^2.5.0",
|
||||||
"@mobicoop/health-module": "^2.3.2",
|
"@mobicoop/health-module": "^2.3.2",
|
||||||
"@mobicoop/message-broker-module": "^2.1.2",
|
"@mobicoop/message-broker-module": "^2.1.2",
|
||||||
"@nestjs/common": "^10.3.0",
|
"@nestjs/common": "^10.3.0",
|
||||||
|
|
|
@ -7,6 +7,8 @@ export const GRPC_SERVICE_NAME = 'AdService';
|
||||||
|
|
||||||
// messaging output
|
// messaging output
|
||||||
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
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
|
// messaging input
|
||||||
export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated';
|
export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated';
|
||||||
|
@ -18,6 +20,10 @@ export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
||||||
'matcher-ad.creation-failed';
|
'matcher-ad.creation-failed';
|
||||||
export const MATCHER_AD_CREATION_FAILED_QUEUE = 'ad.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
|
// configuration
|
||||||
export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set';
|
export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set';
|
||||||
export const SERVICE_CONFIGURATION_DELETE_QUEUE = 'ad-configuration-delete';
|
export const SERVICE_CONFIGURATION_DELETE_QUEUE = 'ad-configuration-delete';
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { Mapper } from '@mobicoop/ddd-library';
|
import { Mapper } from '@mobicoop/ddd-library';
|
||||||
import { AdResponseDto } from './interface/dtos/ad.response.dto';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AdEntity } from './core/domain/ad.entity';
|
|
||||||
import {
|
|
||||||
AdWriteModel,
|
|
||||||
AdReadModel,
|
|
||||||
WaypointModel,
|
|
||||||
ScheduleItemModel,
|
|
||||||
} from './infrastructure/ad.repository';
|
|
||||||
import { Frequency, Status } from './core/domain/ad.types';
|
|
||||||
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
|
|
||||||
import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens';
|
import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens';
|
||||||
import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port';
|
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:
|
* Mapper constructs objects that are used in different layers:
|
||||||
|
@ -31,9 +33,8 @@ export class AdMapper
|
||||||
private readonly outputDatetimeTransformer: DateTimeTransformerPort,
|
private readonly outputDatetimeTransformer: DateTimeTransformerPort,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
toPersistence = (entity: AdEntity): AdWriteModel => {
|
toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
|
||||||
const copy = entity.getProps();
|
const copy = entity.getProps();
|
||||||
const now = new Date();
|
|
||||||
const record: AdWriteModel = {
|
const record: AdWriteModel = {
|
||||||
uuid: copy.id,
|
uuid: copy.id,
|
||||||
userUuid: copy.userId,
|
userUuid: copy.userId,
|
||||||
|
@ -43,50 +44,80 @@ export class AdMapper
|
||||||
frequency: copy.frequency,
|
frequency: copy.frequency,
|
||||||
fromDate: new Date(copy.fromDate),
|
fromDate: new Date(copy.fromDate),
|
||||||
toDate: new Date(copy.toDate),
|
toDate: new Date(copy.toDate),
|
||||||
schedule: copy.schedule
|
schedule: this.toScheduleItemWriteModel(copy.schedule, update),
|
||||||
? {
|
|
||||||
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,
|
seatsProposed: copy.seatsProposed as number,
|
||||||
seatsRequested: copy.seatsRequested as number,
|
seatsRequested: copy.seatsRequested as number,
|
||||||
strict: copy.strict as boolean,
|
strict: copy.strict as boolean,
|
||||||
waypoints: copy.waypoints
|
waypoints: this.toWaypointWriteModel(copy.waypoints, update),
|
||||||
? {
|
|
||||||
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,
|
comment: copy.comment,
|
||||||
};
|
};
|
||||||
return record;
|
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,
|
||||||
|
time: new Date(
|
||||||
|
1970,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
parseInt(scheduleItem.time.split(':')[0]),
|
||||||
|
parseInt(scheduleItem.time.split(':')[1]),
|
||||||
|
),
|
||||||
|
margin: scheduleItem.margin,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
if (update) {
|
||||||
|
record.deleteMany = {
|
||||||
|
createdAt: { lt: now },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
if (update) {
|
||||||
|
record.deleteMany = {
|
||||||
|
createdAt: { lt: now },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
toDomain = (record: AdReadModel): AdEntity => {
|
toDomain = (record: AdReadModel): AdEntity => {
|
||||||
const entity = new AdEntity({
|
const entity = new AdEntity({
|
||||||
id: record.uuid,
|
id: record.uuid,
|
||||||
|
@ -141,6 +172,7 @@ export class AdMapper
|
||||||
response.userId = props.userId;
|
response.userId = props.userId;
|
||||||
response.driver = props.driver as boolean;
|
response.driver = props.driver as boolean;
|
||||||
response.passenger = props.passenger as boolean;
|
response.passenger = props.passenger as boolean;
|
||||||
|
response.strict = props.strict;
|
||||||
response.status = props.status;
|
response.status = props.status;
|
||||||
response.frequency = props.frequency;
|
response.frequency = props.frequency;
|
||||||
response.fromDate = this.outputDatetimeTransformer.fromDate(
|
response.fromDate = this.outputDatetimeTransformer.fromDate(
|
||||||
|
@ -163,7 +195,7 @@ export class AdMapper
|
||||||
response.schedule = props.schedule.map(
|
response.schedule = props.schedule.map(
|
||||||
(scheduleItem: ScheduleItemProps) => ({
|
(scheduleItem: ScheduleItemProps) => ({
|
||||||
day: this.outputDatetimeTransformer.day(
|
day: this.outputDatetimeTransformer.day(
|
||||||
scheduleItem.day as number,
|
scheduleItem.day,
|
||||||
{
|
{
|
||||||
date: props.fromDate,
|
date: props.fromDate,
|
||||||
time: scheduleItem.time,
|
time: scheduleItem.time,
|
||||||
|
@ -179,7 +211,7 @@ export class AdMapper
|
||||||
},
|
},
|
||||||
props.frequency,
|
props.frequency,
|
||||||
),
|
),
|
||||||
margin: scheduleItem.margin as number,
|
margin: scheduleItem.margin,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
response.seatsProposed = props.seatsProposed as number;
|
response.seatsProposed = props.seatsProposed as number;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
import { Module, Provider } from '@nestjs/common';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import {
|
import {
|
||||||
AD_MESSAGE_PUBLISHER,
|
AD_MESSAGE_PUBLISHER,
|
||||||
|
@ -9,29 +9,39 @@ import {
|
||||||
TIMEZONE_FINDER,
|
TIMEZONE_FINDER,
|
||||||
TIME_CONVERTER,
|
TIME_CONVERTER,
|
||||||
} from './ad.di-tokens';
|
} from './ad.di-tokens';
|
||||||
import { AdRepository } from './infrastructure/ad.repository';
|
|
||||||
import { AdMapper } from './ad.mapper';
|
import { AdMapper } from './ad.mapper';
|
||||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
|
||||||
import { TimeConverter } from './infrastructure/time-converter';
|
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service';
|
||||||
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
|
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
|
||||||
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
|
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 { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
|
||||||
import { PrismaService } from './infrastructure/prisma.service';
|
import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler';
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
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 { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||||
import { OutputDateTimeTransformer } from './infrastructure/output-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 { 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 { 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';
|
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 = [
|
const grpcControllers = [
|
||||||
CreateAdGrpcController,
|
CreateAdGrpcController,
|
||||||
|
UpdateAdGrpcController,
|
||||||
|
DeleteAdGrpcController,
|
||||||
FindAdByIdGrpcController,
|
FindAdByIdGrpcController,
|
||||||
FindAdsByIdsGrpcController,
|
FindAdsByIdsGrpcController,
|
||||||
FindAdsByUserIdGrpcController,
|
FindAdsByUserIdGrpcController,
|
||||||
|
@ -40,14 +50,20 @@ const grpcControllers = [
|
||||||
const messageHandlers = [
|
const messageHandlers = [
|
||||||
MatcherAdCreatedMessageHandler,
|
MatcherAdCreatedMessageHandler,
|
||||||
MatcherAdCreationFailedMessageHandler,
|
MatcherAdCreationFailedMessageHandler,
|
||||||
|
UserDeletedMessageHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
const eventHandlers: Provider[] = [
|
const eventHandlers: Provider[] = [
|
||||||
PublishMessageWhenAdIsCreatedDomainEventHandler,
|
PublishMessageWhenAdIsCreatedDomainEventHandler,
|
||||||
|
PublishMessageWhenAdIsUpdatedDomainEventHandler,
|
||||||
|
PublishMessageWhenAdIsDeletedDomainEventHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
const commandHandlers: Provider[] = [
|
const commandHandlers: Provider[] = [
|
||||||
CreateAdService,
|
CreateAdService,
|
||||||
|
UpdateAdService,
|
||||||
|
DeleteAdService,
|
||||||
|
DeleteUserAdsService,
|
||||||
ValidateAdService,
|
ValidateAdService,
|
||||||
InvalidateAdService,
|
InvalidateAdService,
|
||||||
];
|
];
|
||||||
|
|
|
@ -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 { 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 {
|
export class CreateAdCommand extends Command {
|
||||||
readonly userId: string;
|
readonly userId: string;
|
||||||
|
@ -10,7 +10,7 @@ export class CreateAdCommand extends Command {
|
||||||
readonly frequency: Frequency;
|
readonly frequency: Frequency;
|
||||||
readonly fromDate: string;
|
readonly fromDate: string;
|
||||||
readonly toDate: string;
|
readonly toDate: string;
|
||||||
readonly schedule: ScheduleItem[];
|
readonly schedule: ScheduleItemProps[];
|
||||||
readonly seatsProposed?: number;
|
readonly seatsProposed?: number;
|
||||||
readonly seatsRequested?: number;
|
readonly seatsRequested?: number;
|
||||||
readonly strict: boolean;
|
readonly strict: boolean;
|
||||||
|
|
|
@ -1,17 +1,98 @@
|
||||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||||
import { CreateAdCommand } from './create-ad.command';
|
|
||||||
import { Inject } from '@nestjs/common';
|
|
||||||
import {
|
import {
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} from '@modules/ad/ad.di-tokens';
|
||||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { Waypoint } from '../../types/waypoint';
|
|
||||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||||
import { ScheduleItem } from '../../types/schedule-item';
|
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 { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||||
|
import { Waypoint } from '../../types/waypoint';
|
||||||
|
import { CreateAdCommand } from './create-ad.command';
|
||||||
|
|
||||||
|
export function createPropsFromCommand(
|
||||||
|
command: CreateAdCommand,
|
||||||
|
datetimeTransformer: DateTimeTransformerPort,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
userId: command.userId,
|
||||||
|
driver: command.driver,
|
||||||
|
passenger: command.passenger,
|
||||||
|
frequency: command.frequency,
|
||||||
|
//TODO Shouldn't that kind of logic be in the domain layer?
|
||||||
|
fromDate: datetimeTransformer.fromDate(
|
||||||
|
{
|
||||||
|
date: command.fromDate,
|
||||||
|
time: command.schedule[0].time,
|
||||||
|
coordinates: {
|
||||||
|
lon: command.waypoints[0].lon,
|
||||||
|
lat: command.waypoints[0].lat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command.frequency,
|
||||||
|
),
|
||||||
|
toDate: datetimeTransformer.toDate(
|
||||||
|
command.toDate,
|
||||||
|
{
|
||||||
|
date: command.fromDate,
|
||||||
|
time: command.schedule[0].time,
|
||||||
|
coordinates: {
|
||||||
|
lon: command.waypoints[0].lon,
|
||||||
|
lat: command.waypoints[0].lat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command.frequency,
|
||||||
|
),
|
||||||
|
schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||||
|
day: datetimeTransformer.day(
|
||||||
|
scheduleItem.day,
|
||||||
|
{
|
||||||
|
date: command.fromDate,
|
||||||
|
time: scheduleItem.time,
|
||||||
|
coordinates: {
|
||||||
|
lon: command.waypoints[0].lon,
|
||||||
|
lat: command.waypoints[0].lat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command.frequency,
|
||||||
|
),
|
||||||
|
time: datetimeTransformer.time(
|
||||||
|
{
|
||||||
|
date: command.fromDate,
|
||||||
|
time: scheduleItem.time,
|
||||||
|
coordinates: {
|
||||||
|
lon: command.waypoints[0].lon,
|
||||||
|
lat: command.waypoints[0].lat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command.frequency,
|
||||||
|
),
|
||||||
|
margin: scheduleItem.margin,
|
||||||
|
})),
|
||||||
|
seatsProposed: command.seatsProposed ?? 0,
|
||||||
|
seatsRequested: command.seatsRequested ?? 0,
|
||||||
|
strict: command.strict,
|
||||||
|
waypoints: command.waypoints.map((waypoint: Waypoint) => ({
|
||||||
|
position: waypoint.position,
|
||||||
|
address: {
|
||||||
|
name: waypoint.name,
|
||||||
|
houseNumber: waypoint.houseNumber,
|
||||||
|
street: waypoint.street,
|
||||||
|
postalCode: waypoint.postalCode,
|
||||||
|
locality: waypoint.locality,
|
||||||
|
country: waypoint.country,
|
||||||
|
coordinates: {
|
||||||
|
lon: waypoint.lon,
|
||||||
|
lat: waypoint.lat,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
comment: command.comment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@CommandHandler(CreateAdCommand)
|
@CommandHandler(CreateAdCommand)
|
||||||
export class CreateAdService implements ICommandHandler {
|
export class CreateAdService implements ICommandHandler {
|
||||||
|
@ -23,80 +104,9 @@ export class CreateAdService implements ICommandHandler {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||||
const ad = AdEntity.create({
|
const ad = AdEntity.create(
|
||||||
userId: command.userId,
|
createPropsFromCommand(command, this.datetimeTransformer),
|
||||||
driver: command.driver,
|
);
|
||||||
passenger: command.passenger,
|
|
||||||
frequency: command.frequency,
|
|
||||||
fromDate: this.datetimeTransformer.fromDate(
|
|
||||||
{
|
|
||||||
date: command.fromDate,
|
|
||||||
time: command.schedule[0].time,
|
|
||||||
coordinates: {
|
|
||||||
lon: command.waypoints[0].lon,
|
|
||||||
lat: command.waypoints[0].lat,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
command.frequency,
|
|
||||||
),
|
|
||||||
toDate: this.datetimeTransformer.toDate(
|
|
||||||
command.toDate,
|
|
||||||
{
|
|
||||||
date: command.fromDate,
|
|
||||||
time: command.schedule[0].time,
|
|
||||||
coordinates: {
|
|
||||||
lon: command.waypoints[0].lon,
|
|
||||||
lat: command.waypoints[0].lat,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
command.frequency,
|
|
||||||
),
|
|
||||||
schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
|
|
||||||
day: this.datetimeTransformer.day(
|
|
||||||
scheduleItem.day,
|
|
||||||
{
|
|
||||||
date: command.fromDate,
|
|
||||||
time: scheduleItem.time,
|
|
||||||
coordinates: {
|
|
||||||
lon: command.waypoints[0].lon,
|
|
||||||
lat: command.waypoints[0].lat,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
command.frequency,
|
|
||||||
),
|
|
||||||
time: this.datetimeTransformer.time(
|
|
||||||
{
|
|
||||||
date: command.fromDate,
|
|
||||||
time: scheduleItem.time,
|
|
||||||
coordinates: {
|
|
||||||
lon: command.waypoints[0].lon,
|
|
||||||
lat: command.waypoints[0].lat,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
command.frequency,
|
|
||||||
),
|
|
||||||
margin: scheduleItem.margin,
|
|
||||||
})),
|
|
||||||
seatsProposed: command.seatsProposed ?? 0,
|
|
||||||
seatsRequested: command.seatsRequested ?? 0,
|
|
||||||
strict: command.strict,
|
|
||||||
waypoints: command.waypoints.map((waypoint: Waypoint) => ({
|
|
||||||
position: waypoint.position,
|
|
||||||
address: {
|
|
||||||
name: waypoint.name,
|
|
||||||
houseNumber: waypoint.houseNumber,
|
|
||||||
street: waypoint.street,
|
|
||||||
postalCode: waypoint.postalCode,
|
|
||||||
locality: waypoint.locality,
|
|
||||||
country: waypoint.country,
|
|
||||||
coordinates: {
|
|
||||||
lon: waypoint.lon,
|
|
||||||
lat: waypoint.lat,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
comment: command.comment,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.repository.insert(ad);
|
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,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,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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
export type ScheduleItem = {
|
|
||||||
day: number;
|
|
||||||
time: string;
|
|
||||||
margin: number;
|
|
||||||
};
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Address } from './address';
|
import { Address } from './address';
|
||||||
|
|
||||||
|
//TODO Why not use the Waypoint value-object from the domain?
|
||||||
export type Waypoint = {
|
export type Waypoint = {
|
||||||
position: number;
|
position: number;
|
||||||
} & Address;
|
} & Address;
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { AdCreatedDomainEvent } from './events/ad-created.domain-event';
|
|
||||||
import { AdProps, CreateAdProps, Status } from './ad.types';
|
import { AdProps, CreateAdProps, Status } from './ad.types';
|
||||||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
import { AdCreatedDomainEvent } from './events/ad-created.domain-event';
|
||||||
import { WaypointProps } from './value-objects/waypoint.value-object';
|
import { AdDeletedDomainEvent } from './events/ad-delete.domain-event';
|
||||||
import { AdValidatedDomainEvent } from './events/ad-validated.domain-event';
|
|
||||||
import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event';
|
import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event';
|
||||||
import { AdSuspendedDomainEvent } from './events/ad-suspended.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';
|
||||||
|
|
||||||
export class AdEntity extends AggregateRoot<AdProps> {
|
export class AdEntity extends AggregateRoot<AdProps> {
|
||||||
protected readonly _id: AggregateID;
|
protected readonly _id: AggregateID;
|
||||||
|
@ -29,9 +30,9 @@ export class AdEntity extends AggregateRoot<AdProps> {
|
||||||
fromDate: props.fromDate,
|
fromDate: props.fromDate,
|
||||||
toDate: props.toDate,
|
toDate: props.toDate,
|
||||||
schedule: props.schedule.map((day: ScheduleItemProps) => ({
|
schedule: props.schedule.map((day: ScheduleItemProps) => ({
|
||||||
day: day.day as number,
|
day: day.day,
|
||||||
time: day.time,
|
time: day.time,
|
||||||
margin: day.margin as number,
|
margin: day.margin,
|
||||||
})),
|
})),
|
||||||
seatsProposed: props.seatsProposed,
|
seatsProposed: props.seatsProposed,
|
||||||
seatsRequested: props.seatsRequested,
|
seatsRequested: props.seatsRequested,
|
||||||
|
@ -95,6 +96,32 @@ export class AdEntity extends AggregateRoot<AdProps> {
|
||||||
return this;
|
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 {
|
validate(): void {
|
||||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,6 @@
|
||||||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
|
||||||
import { WaypointProps } from './value-objects/waypoint.value-object';
|
import { WaypointProps } from './value-objects/waypoint.value-object';
|
||||||
|
|
||||||
// All properties that an Ad has
|
|
||||||
export interface AdProps {
|
|
||||||
userId: string;
|
|
||||||
driver: boolean;
|
|
||||||
status: Status;
|
|
||||||
passenger: boolean;
|
|
||||||
frequency: Frequency;
|
|
||||||
fromDate: string;
|
|
||||||
toDate: string;
|
|
||||||
schedule: ScheduleItemProps[];
|
|
||||||
seatsProposed: number;
|
|
||||||
seatsRequested: number;
|
|
||||||
strict: boolean;
|
|
||||||
waypoints: WaypointProps[];
|
|
||||||
comment?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Properties that are needed for an Ad creation
|
// Properties that are needed for an Ad creation
|
||||||
export interface CreateAdProps {
|
export interface CreateAdProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -34,6 +17,11 @@ export interface CreateAdProps {
|
||||||
comment?: string;
|
comment?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All properties that an Ad has
|
||||||
|
export interface AdProps extends CreateAdProps {
|
||||||
|
status: Status;
|
||||||
|
}
|
||||||
|
|
||||||
export enum Frequency {
|
export enum Frequency {
|
||||||
PUNCTUAL = 'PUNCTUAL',
|
PUNCTUAL = 'PUNCTUAL',
|
||||||
RECURRENT = 'RECURRENT',
|
RECURRENT = 'RECURRENT',
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
import { ScheduleItemProps } from '../value-objects/schedule-item.value-object';
|
||||||
|
|
||||||
export class AdCreatedDomainEvent extends DomainEvent {
|
export class AdCreatedDomainEvent extends DomainEvent {
|
||||||
readonly userId: string;
|
readonly userId: string;
|
||||||
|
@ -7,7 +8,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
|
||||||
readonly frequency: string;
|
readonly frequency: string;
|
||||||
readonly fromDate: string;
|
readonly fromDate: string;
|
||||||
readonly toDate: string;
|
readonly toDate: string;
|
||||||
readonly schedule: ScheduleItem[];
|
readonly schedule: ScheduleItemProps[];
|
||||||
readonly seatsProposed: number;
|
readonly seatsProposed: number;
|
||||||
readonly seatsRequested: number;
|
readonly seatsRequested: number;
|
||||||
readonly strict: boolean;
|
readonly strict: boolean;
|
||||||
|
@ -31,12 +32,6 @@ export class AdCreatedDomainEvent extends DomainEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScheduleItem {
|
|
||||||
day: number;
|
|
||||||
time: string;
|
|
||||||
margin: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Waypoint {
|
export class Waypoint {
|
||||||
position: number;
|
position: number;
|
||||||
name?: string;
|
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,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 {
|
export interface ScheduleItemProps {
|
||||||
day?: number;
|
day: number;
|
||||||
time: string;
|
time: string;
|
||||||
margin?: number;
|
margin: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
|
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
|
||||||
get day(): number | undefined {
|
get day(): number {
|
||||||
return this.props.day;
|
return this.props.day;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export class ScheduleItem extends ValueObject<ScheduleItemProps> {
|
||||||
return this.props.time;
|
return this.props.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
get margin(): number | undefined {
|
get margin(): number {
|
||||||
return this.props.margin;
|
return this.props.margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
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 {
|
import {
|
||||||
LoggerBase,
|
LoggerBase,
|
||||||
MessagePublisherPort,
|
MessagePublisherPort,
|
||||||
PrismaRepositoryBase,
|
PrismaRepositoryBase,
|
||||||
} from '@mobicoop/ddd-library';
|
} from '@mobicoop/ddd-library';
|
||||||
import { PrismaService } from './prisma.service';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { SERVICE_NAME } from '@src/app.constants';
|
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 = {
|
export type AdBaseModel = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
@ -40,13 +40,21 @@ export type AdWriteModel = AdBaseModel & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScheduleWriteModel = {
|
export type ScheduleWriteModel = {
|
||||||
|
deleteMany?: PastCreatedFilter;
|
||||||
create: ScheduleItemModel[];
|
create: ScheduleItemModel[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WaypointWriteModel = {
|
export type WaypointWriteModel = {
|
||||||
|
deleteMany?: PastCreatedFilter;
|
||||||
create: WaypointModel[];
|
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 = {
|
export type ScheduleItemModel = {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
day: number;
|
day: number;
|
||||||
|
|
|
@ -7,7 +7,7 @@ service AdService {
|
||||||
rpc FindAllByIds(AdsById) returns (Ads);
|
rpc FindAllByIds(AdsById) returns (Ads);
|
||||||
rpc FindAllByUserId(UserById) returns (Ads);
|
rpc FindAllByUserId(UserById) returns (Ads);
|
||||||
rpc Create(Ad) returns (AdById);
|
rpc Create(Ad) returns (AdById);
|
||||||
rpc Update(Ad) returns (Ad);
|
rpc Update(Ad) returns (Empty);
|
||||||
rpc Delete(AdById) returns (Empty);
|
rpc Delete(AdById) returns (Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ message Waypoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Frequency {
|
enum Frequency {
|
||||||
|
UNSPECIFIED = 0;
|
||||||
PUNCTUAL = 1;
|
PUNCTUAL = 1;
|
||||||
RECURRENT = 2;
|
RECURRENT = 2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class DeleteAdRequestDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
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,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,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,6 +1,11 @@
|
||||||
import { Module, Provider } from '@nestjs/common';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MessageBrokerModule,
|
||||||
|
MessageBrokerModuleOptions,
|
||||||
|
MessageBrokerPublisher,
|
||||||
|
} from '@mobicoop/message-broker-module';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
MATCHER_AD_CREATED_MESSAGE_HANDLER,
|
MATCHER_AD_CREATED_MESSAGE_HANDLER,
|
||||||
|
@ -10,12 +15,10 @@ import {
|
||||||
MATCHER_AD_CREATION_FAILED_QUEUE,
|
MATCHER_AD_CREATION_FAILED_QUEUE,
|
||||||
MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
|
MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
|
USER_DELETED_MESSAGE_HANDLER,
|
||||||
|
USER_DELETED_QUEUE,
|
||||||
|
USER_DELETED_ROUTING_KEY,
|
||||||
} from '@src/app.constants';
|
} from '@src/app.constants';
|
||||||
import {
|
|
||||||
MessageBrokerModule,
|
|
||||||
MessageBrokerModuleOptions,
|
|
||||||
MessageBrokerPublisher,
|
|
||||||
} from '@mobicoop/message-broker-module';
|
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
MessageBrokerModule.forRootAsync({
|
MessageBrokerModule.forRootAsync({
|
||||||
|
@ -41,6 +44,10 @@ const imports = [
|
||||||
routingKey: MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
|
routingKey: MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
|
||||||
queue: MATCHER_AD_CREATION_FAILED_QUEUE,
|
queue: MATCHER_AD_CREATION_FAILED_QUEUE,
|
||||||
},
|
},
|
||||||
|
[USER_DELETED_MESSAGE_HANDLER]: {
|
||||||
|
routingKey: USER_DELETED_ROUTING_KEY,
|
||||||
|
queue: USER_DELETED_QUEUE,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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(),
|
||||||
|
};
|
||||||
|
}
|
|
@ -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 {
|
import {
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} 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 { 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 { 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 = {
|
const originWaypoint: WaypointDto = {
|
||||||
position: 0,
|
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', () => {
|
describe('create-ad.service', () => {
|
||||||
let createAdService: CreateAdService;
|
let createAdService: CreateAdService;
|
||||||
|
|
||||||
|
@ -83,7 +75,7 @@ describe('create-ad.service', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: INPUT_DATETIME_TRANSFORMER,
|
provide: INPUT_DATETIME_TRANSFORMER,
|
||||||
useValue: mockInputDateTimeTransformer,
|
useValue: mockInputDateTimeTransformer(),
|
||||||
},
|
},
|
||||||
CreateAdService,
|
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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,62 +1,11 @@
|
||||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
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 { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
|
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 { 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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { punctualPassengerCreateAdProps } from './ad.fixtures';
|
||||||
|
|
||||||
const originWaypointProps: WaypointProps = {
|
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps());
|
||||||
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 = {
|
const mockAdRepository = {
|
||||||
findOneById: jest.fn().mockImplementation(() => ad),
|
findOneById: jest.fn().mockImplementation(() => ad),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
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 = {
|
const originWaypointProps: WaypointProps = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -44,7 +44,9 @@ const punctualCreateAdProps = {
|
||||||
toDate: '2023-06-22',
|
toDate: '2023-06-22',
|
||||||
schedule: [
|
schedule: [
|
||||||
{
|
{
|
||||||
|
day: 4,
|
||||||
time: '08:30',
|
time: '08:30',
|
||||||
|
margin: 900,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
frequency: Frequency.PUNCTUAL,
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
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 = {
|
const originWaypointProps: WaypointProps = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -44,7 +44,9 @@ const punctualCreateAdProps = {
|
||||||
toDate: '2023-06-22',
|
toDate: '2023-06-22',
|
||||||
schedule: [
|
schedule: [
|
||||||
{
|
{
|
||||||
|
day: 4,
|
||||||
time: '08:30',
|
time: '08:30',
|
||||||
|
margin: 900,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
frequency: Frequency.PUNCTUAL,
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
|
||||||
import { AggregateID } from '@mobicoop/ddd-library';
|
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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
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 { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
|
|
||||||
|
|
||||||
const originWaypointProps: WaypointProps = {
|
const originWaypointProps: WaypointProps = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -45,7 +45,9 @@ const punctualCreateAdProps = {
|
||||||
toDate: '2023-06-22',
|
toDate: '2023-06-22',
|
||||||
schedule: [
|
schedule: [
|
||||||
{
|
{
|
||||||
|
day: 4,
|
||||||
time: '08:30',
|
time: '08:30',
|
||||||
|
margin: 900,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
frequency: Frequency.PUNCTUAL,
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
|
|
@ -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,11 +1,11 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
|
||||||
import { AggregateID } from '@mobicoop/ddd-library';
|
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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
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';
|
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
const originWaypointProps: WaypointProps = {
|
const originWaypointProps: WaypointProps = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -45,7 +45,9 @@ const punctualCreateAdProps = {
|
||||||
toDate: '2023-06-22',
|
toDate: '2023-06-22',
|
||||||
schedule: [
|
schedule: [
|
||||||
{
|
{
|
||||||
|
day: 4,
|
||||||
time: '08:30',
|
time: '08:30',
|
||||||
|
margin: 900,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
frequency: Frequency.PUNCTUAL,
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
|
|
@ -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 { IdResponse, RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
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 { 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 { CommandBus } from '@nestjs/cqrs';
|
||||||
import { RpcException } from '@nestjs/microservices';
|
import { RpcException } from '@nestjs/microservices';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { punctualCreateAdRequest } from './ad.fixtures';
|
||||||
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],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockCommandBus = {
|
const mockCommandBus = {
|
||||||
execute: jest
|
execute: jest
|
||||||
|
@ -89,7 +48,7 @@ describe('Create Ad Grpc Controller', () => {
|
||||||
it('should create a new ad', async () => {
|
it('should create a new ad', async () => {
|
||||||
jest.spyOn(mockCommandBus, 'execute');
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
const result: IdResponse = await createAdGrpcController.create(
|
const result: IdResponse = await createAdGrpcController.create(
|
||||||
punctualCreateAdRequest,
|
punctualCreateAdRequest(),
|
||||||
);
|
);
|
||||||
expect(result).toBeInstanceOf(IdResponse);
|
expect(result).toBeInstanceOf(IdResponse);
|
||||||
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
|
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
|
||||||
|
@ -100,7 +59,7 @@ describe('Create Ad Grpc Controller', () => {
|
||||||
jest.spyOn(mockCommandBus, 'execute');
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
try {
|
try {
|
||||||
await createAdGrpcController.create(punctualCreateAdRequest);
|
await createAdGrpcController.create(punctualCreateAdRequest());
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
expect(e).toBeInstanceOf(RpcException);
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
|
||||||
|
@ -112,7 +71,7 @@ describe('Create Ad Grpc Controller', () => {
|
||||||
jest.spyOn(mockCommandBus, 'execute');
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
try {
|
try {
|
||||||
await createAdGrpcController.create(punctualCreateAdRequest);
|
await createAdGrpcController.create(punctualCreateAdRequest());
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
expect(e).toBeInstanceOf(RpcException);
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
|
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,76 @@
|
||||||
|
import { NotFoundException, RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import { UpdateAdGrpcController } from '@modules/ad/interface/grpc-controllers/update-ad.grpc.controller';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { RpcException } from '@nestjs/microservices';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { punctualCreateAdRequest } from './ad.fixtures';
|
||||||
|
|
||||||
|
const validAdId = '200d61a8-d878-4378-a609-c19ea71633d2';
|
||||||
|
const mockCommandBus = {
|
||||||
|
execute: jest.fn().mockImplementation(async (command) => {
|
||||||
|
if (command.adId === '') throw 'Ad id is empty';
|
||||||
|
if (command.adId != validAdId) throw new NotFoundException();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Update Ad GRPC Controller', () => {
|
||||||
|
let updateAdGrpcController: UpdateAdGrpcController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CommandBus,
|
||||||
|
useValue: mockCommandBus,
|
||||||
|
},
|
||||||
|
UpdateAdGrpcController,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
updateAdGrpcController = module.get<UpdateAdGrpcController>(
|
||||||
|
UpdateAdGrpcController,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(updateAdGrpcController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute the update ad command', async () => {
|
||||||
|
await updateAdGrpcController.update({
|
||||||
|
id: validAdId,
|
||||||
|
...punctualCreateAdRequest(),
|
||||||
|
});
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a dedicated RpcException if ad is not found', async () => {
|
||||||
|
expect.assertions(3);
|
||||||
|
try {
|
||||||
|
await updateAdGrpcController.update({
|
||||||
|
id: 'ac85f5f4-41cd-4c5d-9aee-0a1acb176fb8',
|
||||||
|
...punctualCreateAdRequest(),
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(RpcException);
|
||||||
|
expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rethrow any other exceptions', async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
try {
|
||||||
|
await updateAdGrpcController.update({
|
||||||
|
id: '',
|
||||||
|
...punctualCreateAdRequest(),
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBe('Ad id is empty');
|
||||||
|
}
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { UserDeletedMessageHandler } from '@modules/ad/interface/message-handlers/user-deleted.message-handler';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const mockCommandBus = {
|
||||||
|
execute: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Matcher Ad Created Message Handler', () => {
|
||||||
|
let userDeletedMessageHandler: UserDeletedMessageHandler;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CommandBus,
|
||||||
|
useValue: mockCommandBus,
|
||||||
|
},
|
||||||
|
UserDeletedMessageHandler,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
userDeletedMessageHandler = module.get<UserDeletedMessageHandler>(
|
||||||
|
UserDeletedMessageHandler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(userDeletedMessageHandler).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the command bus', async () => {
|
||||||
|
const userId = '4eb6a6af-ecfd-41c3-9118-473a507014d4';
|
||||||
|
const userDeletedMessage = `{"id":"${userId}"}`;
|
||||||
|
await userDeletedMessageHandler.userDeleted(userDeletedMessage);
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockCommandBus.execute.mock.lastCall[0].id).toBe(userId);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
"exclude": ["node_modules", "tests", "dist", "**/*spec.ts"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue