Compare commits

..

No commits in common. "next-release" and "v2.2.4" have entirely different histories.

109 changed files with 1504 additions and 3887 deletions

View File

@ -8,7 +8,7 @@ HEALTH_SERVICE_PORT=6006
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=ad"
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://mobicoop:mobicoop@v3-broker:5672
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
MESSAGE_BROKER_EXCHANGE_DURABILITY=true

View File

@ -4,10 +4,51 @@ stages:
- test
- build
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
- project: mobicoop/v3/gitlab-templates
file:
- /ci/service.test-job.yml
- /ci/release.build-job.yml
##############
# TEST STAGE #
##############
test:
stage: test
image: docker/compose:latest
variables:
DOCKER_TLS_CERTDIR: ''
services:
- docker:dind
script:
- docker-compose -f docker-compose.ci.tools.yml -p ad-tools --env-file ci/.env.ci up -d
- sh ci/wait-up.sh
- docker-compose -f docker-compose.ci.service.yml -p ad-service --env-file ci/.env.ci up -d
- docker exec -t v3-ad-api sh -c "npm run test:integration:ci"
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: always
###############
# BUILD STAGE #
###############
build:
stage: build
image: docker:20.10.22
variables:
DOCKER_TLS_CERTDIR: ''
services:
- docker:dind
before_script:
- echo -n $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- export VERSION=$(docker run --rm -v "$PWD":/usr/src/app:ro -w /usr/src/app node:slim node -p "require('./package.json').version")
- docker pull $CI_REGISTRY_IMAGE:latest || true
- >
docker build
--pull
--cache-from $CI_REGISTRY_IMAGE:latest
--tag $CI_REGISTRY_IMAGE:$VERSION
--tag $CI_REGISTRY_IMAGE:latest
.
- docker push $CI_REGISTRY_IMAGE:$VERSION
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
# BUILD FOR LOCAL DEVELOPMENT
###################
FROM docker.io/node:lts-hydrogen As development
FROM node:18-alpine3.16 As development
# Create app directory
WORKDIR /usr/src/app
@ -29,7 +29,7 @@ USER node
# BUILD FOR PRODUCTION
###################
FROM docker.io/node:lts-hydrogen As build
FROM node:18-alpine3.16 As build
WORKDIR /usr/src/app
@ -63,7 +63,7 @@ USER node
# PRODUCTION
###################
FROM docker.io/node:lts-hydrogen As production
FROM node:18-alpine3.16 As production
# Copy package.json to be able to execute migration command
COPY --chown=node:node package*.json ./

View File

@ -56,26 +56,6 @@ The app exposes the following [gRPC](https://grpc.io/) services :
}
```
- **FindAllByIds** : find all ads for the given ids
```json
{
"ids": [
"80126a61-d128-4f96-afdb-92e33c75a3e1",
"80126a61-d128-4f96-afdb-92e33c75a3e2",
"80126a61-d128-4f96-afdb-92e33c75a3e3"
]
}
```
- **FindAllByUserId** : find all ads for the given user id
```json
{
"id": "80c9bb02-0931-4a1d-bea6-22d358992245"
}
```
- **Create** : create an ad
Punctual driver ad :
@ -115,8 +95,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"postalCode": "75000",
"country": "France"
}
],
"comment": "I'm flexible with the departure time"
]
}
```
@ -158,8 +137,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"postalCode": "75000",
"country": "France"
}
],
"comment": "I'm flexible with the departure time"
]
}
```
@ -209,8 +187,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"postalCode": "75000",
"country": "France"
}
],
"comment": "I'm flexible with the departure time"
]
}
```
@ -230,36 +207,12 @@ The app exposes the following [gRPC](https://grpc.io/) services :
- seatsRequested: number of seats requested as passenger (required if `passenger` is true)
- strict (boolean): if set to true, allow matching only with similar frequency ads
- waypoints: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
- comment: optional freetext comment / description about the ad
- **Update** : Replace the content of an ad
Accepts the same data as the `Create` function + an ad id, and replace the given ad with the given data.
- **Delete** : Delete permanently an ad
```json
{
"id": "80126a61-d128-4f96-afdb-92e33c75a3e1"
}
```
## Messages
### Handled
The service listens to these RabbitMQ messages:
- **matcher-ad.created** (to update the status of pending ads)
- **matcher-ad.creation-failed** (to update the status of pending ads)
- **user.deleted** (to delete the associated ads)
### Emitted
As mentionned earlier, RabbitMQ messages are sent after these events :
- **ad.created** (message: the created ad information)
- **ad.updated** (message: the updated ad information)
- **ad.deleted** (message: the id of the deleted ad)
- **Create** (message : the created ad informations)
## Tests / ESLint / Prettier

View File

@ -9,7 +9,8 @@ DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
RMQ_URI=amqp://v3-ad-broker:5672
# MESSAGE BROKER
BROKER_IMAGE=docker.io/rabbitmq:3-alpine
BROKER_IMAGE=rabbitmq:3-alpine
# POSTGRES
POSTGRES_IMAGE=docker.io/postgres:15.0
POSTGRES_IMAGE=postgres:15.0

View File

@ -2,7 +2,7 @@
# BUILD FOR CI TESTING
###################
FROM docker.io/node:lts-hydrogen
FROM node:18-alpine3.16
# Create app directory
WORKDIR /usr/src/app

View File

@ -11,11 +11,10 @@ services:
- .:/usr/src/app
env_file:
- .env
command: npm run start:debug
command: npm run start:dev
ports:
- ${SERVICE_PORT:-5006}:${SERVICE_PORT:-5006}
- ${HEALTH_SERVICE_PORT:-6006}:${HEALTH_SERVICE_PORT:-6006}
- 9226:9229
networks:
v3-network:
aliases:

2320
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@mobicoop/ad",
"version": "2.6.0",
"version": "2.2.4",
"description": "Mobicoop V3 Ad",
"author": "sbriat",
"private": true,
@ -11,7 +11,7 @@
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
@ -24,61 +24,61 @@
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate deploy'",
"migrate:dev": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'",
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'",
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
"migrate:deploy": "npx prisma migrate deploy"
},
"dependencies": {
"@grpc/grpc-js": "^1.9.14",
"@grpc/grpc-js": "^1.9.8",
"@grpc/proto-loader": "^0.7.10",
"@songkeys/nestjs-redis": "^10.0.0",
"@mobicoop/ddd-library": "^2.5.0",
"@mobicoop/health-module": "^2.3.2",
"@mobicoop/message-broker-module": "^2.1.2",
"@nestjs/common": "^10.3.0",
"@mobicoop/ddd-library": "^2.1.1",
"@mobicoop/health-module": "^2.3.1",
"@mobicoop/message-broker-module": "^2.1.1",
"@nestjs/common": "^10.2.7",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.0",
"@nestjs/core": "^10.2.7",
"@nestjs/cqrs": "^10.2.6",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/microservices": "^10.3.0",
"@nestjs/platform-express": "^10.3.0",
"@nestjs/terminus": "^10.2.0",
"@prisma/client": "^5.8.1",
"@nestjs/event-emitter": "^2.0.2",
"@nestjs/microservices": "^10.2.7",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/terminus": "^10.1.1",
"@prisma/client": "^5.5.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"geo-tz": "^8.0.0",
"class-validator": "^0.14.0",
"geo-tz": "^7.0.7",
"ioredis": "^5.3.2",
"nestjs-request-context": "^3.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"timezonecomplete": "^5.12.4"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
"@nestjs/schematics": "^10.1.0",
"@nestjs/testing": "^10.3.0",
"@types/express": "^4.17.21",
"@types/jest": "29.5.11",
"@types/node": "20.11.5",
"@types/supertest": "^6.0.2",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.7",
"@types/express": "^4.17.20",
"@types/jest": "29.5.6",
"@types/node": "20.8.9",
"@types/supertest": "^2.0.15",
"@types/uuid": "^9.0.6",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "29.7.0",
"prettier": "^3.2.3",
"prisma": "^5.8.1",
"prettier": "^3.0.3",
"prisma": "^5.5.2",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"supertest": "^6.3.3",
"ts-jest": "29.1.1",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "4.2.0",
"typescript": "^5.3.3"
"typescript": "^5.2.2"
},
"jest": {
"moduleFileExtensions": [
@ -96,13 +96,13 @@
"prisma.service.ts",
"main.ts"
],
"rootDir": ".",
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"./src/**/*.(t|j)s"
"**/*.(t|j)s"
],
"coveragePathIgnorePatterns": [
".constants.ts",
@ -114,10 +114,10 @@
"prisma.service.ts",
"main.ts"
],
"coverageDirectory": "coverage",
"coverageDirectory": "../coverage",
"moduleNameMapper": {
"^@modules(.*)": "<rootDir>/src/modules/$1",
"^@src(.*)": "<rootDir>/src/$1"
"^@modules(.*)": "<rootDir>/modules/$1",
"^@src(.*)": "<rootDir>$1"
},
"testEnvironment": "node"
}

View File

@ -1,5 +0,0 @@
-- CreateEnum
CREATE TYPE "Status" AS ENUM ('PENDING', 'VALID', 'INVALID', 'SUSPENDED');
-- AlterTable
ALTER TABLE "ad" ADD COLUMN "status" "Status" NOT NULL DEFAULT 'PENDING';

View File

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

View File

@ -14,7 +14,6 @@ datasource db {
model Ad {
uuid String @id @default(uuid()) @db.Uuid
userUuid String @db.Uuid
status Status @default(PENDING)
driver Boolean
passenger Boolean
frequency Frequency
@ -27,7 +26,6 @@ model Ad {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
waypoints Waypoint[]
comment String?
@@map("ad")
}
@ -68,10 +66,3 @@ enum Frequency {
PUNCTUAL
RECURRENT
}
enum Status {
PENDING
VALID
INVALID
SUSPENDED
}

View File

@ -5,24 +5,8 @@ export const SERVICE_NAME = 'ad';
export const GRPC_PACKAGE_NAME = 'ad';
export const GRPC_SERVICE_NAME = 'AdService';
// messaging output
// messaging
export const AD_CREATED_ROUTING_KEY = 'ad.created';
export const AD_UPDATED_ROUTING_KEY = 'ad.updated';
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
// messaging input
export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated';
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
export const MATCHER_AD_CREATED_QUEUE = 'ad.matcher-ad.created';
export const MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER =
'matcherAdCreationFailed';
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
'matcher-ad.creation-failed';
export const MATCHER_AD_CREATION_FAILED_QUEUE = 'ad.matcher-ad.creation-failed';
export const USER_DELETED_MESSAGE_HANDLER = 'userDeleted';
export const USER_DELETED_ROUTING_KEY = 'user.deleted';
export const USER_DELETED_QUEUE = 'ad.user.deleted';
// configuration
export const SERVICE_CONFIGURATION_SET_QUEUE = 'ad-configuration-set';

View File

@ -1,21 +1,19 @@
import { Mapper } from '@mobicoop/ddd-library';
import { AdResponseDto } from './interface/dtos/ad.response.dto';
import { Inject, Injectable } from '@nestjs/common';
import { AdEntity } from './core/domain/ad.entity';
import {
AdWriteModel,
AdReadModel,
WaypointModel,
ScheduleItemModel,
} from './infrastructure/ad.repository';
import { Frequency } from './core/domain/ad.types';
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
import { v4 } from 'uuid';
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens';
import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port';
import { AdEntity } from './core/domain/ad.entity';
import { Frequency, Status } from './core/domain/ad.types';
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
import {
AdReadModel,
AdWriteModel,
ScheduleItemModel,
ScheduleWriteModel,
WaypointModel,
WaypointWriteModel,
} from './infrastructure/ad.repository';
import { AdResponseDto } from './interface/dtos/ad.response.dto';
/**
* Mapper constructs objects that are used in different layers:
@ -33,39 +31,21 @@ export class AdMapper
private readonly outputDatetimeTransformer: DateTimeTransformerPort,
) {}
toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
toPersistence = (entity: AdEntity): AdWriteModel => {
const copy = entity.getProps();
const now = new Date();
const record: AdWriteModel = {
uuid: copy.id,
userUuid: copy.userId,
driver: copy.driver as boolean,
passenger: copy.passenger as boolean,
status: copy.status,
frequency: copy.frequency,
fromDate: new Date(copy.fromDate),
toDate: new Date(copy.toDate),
schedule: this.toScheduleItemWriteModel(copy.schedule, update),
seatsProposed: copy.seatsProposed as number,
seatsRequested: copy.seatsRequested as number,
strict: copy.strict as boolean,
waypoints: this.toWaypointWriteModel(copy.waypoints, update),
comment: copy.comment,
};
return record;
};
toScheduleItemWriteModel = (
schedule: ScheduleItemProps[],
update?: boolean,
): ScheduleWriteModel | undefined => {
if (!schedule) {
return undefined;
}
const now = new Date();
const record: ScheduleWriteModel = {
create: schedule.map((scheduleItem: ScheduleItemProps) => ({
schedule: {
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
uuid: v4(),
day: scheduleItem.day,
day: scheduleItem.day as number,
time: new Date(
1970,
0,
@ -73,29 +53,16 @@ export class AdMapper
parseInt(scheduleItem.time.split(':')[0]),
parseInt(scheduleItem.time.split(':')[1]),
),
margin: scheduleItem.margin,
margin: scheduleItem.margin as number,
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) => ({
},
seatsProposed: copy.seatsProposed as number,
seatsRequested: copy.seatsRequested as number,
strict: copy.strict as boolean,
waypoints: {
create: copy.waypoints.map((waypoint: WaypointProps) => ({
uuid: v4(),
position: waypoint.position,
name: waypoint.address.name,
@ -109,12 +76,10 @@ export class AdMapper
createdAt: now,
updatedAt: now,
})),
},
createdAt: copy.createdAt,
updatedAt: copy.updatedAt,
};
if (update) {
record.deleteMany = {
createdAt: { lt: now },
};
}
return record;
};
@ -127,11 +92,10 @@ export class AdMapper
userId: record.userUuid,
driver: record.driver,
passenger: record.passenger,
status: record.status as Status,
frequency: record.frequency as Frequency,
fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0],
schedule: record.schedule?.map((scheduleItem: ScheduleItemModel) => ({
schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({
day: scheduleItem.day,
time: `${scheduleItem.time
.getUTCHours()
@ -145,7 +109,7 @@ export class AdMapper
seatsProposed: record.seatsProposed,
seatsRequested: record.seatsRequested,
strict: record.strict,
waypoints: record.waypoints?.map((waypoint: WaypointModel) => ({
waypoints: record.waypoints.map((waypoint: WaypointModel) => ({
position: waypoint.position,
address: {
name: waypoint.name,
@ -160,7 +124,6 @@ export class AdMapper
},
},
})),
comment: record.comment,
},
});
return entity;
@ -172,8 +135,6 @@ export class AdMapper
response.userId = props.userId;
response.driver = props.driver as boolean;
response.passenger = props.passenger as boolean;
response.strict = props.strict;
response.status = props.status;
response.frequency = props.frequency;
response.fromDate = this.outputDatetimeTransformer.fromDate(
{
@ -195,7 +156,7 @@ export class AdMapper
response.schedule = props.schedule.map(
(scheduleItem: ScheduleItemProps) => ({
day: this.outputDatetimeTransformer.day(
scheduleItem.day,
scheduleItem.day as number,
{
date: props.fromDate,
time: scheduleItem.time,
@ -211,7 +172,7 @@ export class AdMapper
},
props.frequency,
),
margin: scheduleItem.margin,
margin: scheduleItem.margin as number,
}),
);
response.seatsProposed = props.seatsProposed as number;
@ -227,7 +188,6 @@ export class AdMapper
lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat,
}));
response.comment = props.comment;
return response;
};
}

View File

@ -1,5 +1,5 @@
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { Module, Provider } from '@nestjs/common';
import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
import { CqrsModule } from '@nestjs/cqrs';
import {
AD_MESSAGE_PUBLISHER,
@ -9,70 +9,28 @@ import {
TIMEZONE_FINDER,
TIME_CONVERTER,
} from './ad.di-tokens';
import { AdRepository } from './infrastructure/ad.repository';
import { AdMapper } from './ad.mapper';
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service';
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service';
import { PublishMessageWhenAdIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-deleted.domain-event-handler';
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler';
import { TimezoneFinder } from './infrastructure/timezone-finder';
import { TimeConverter } from './infrastructure/time-converter';
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
import { 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 { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
import { PrismaService } from './infrastructure/prisma.service';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
import { PrismaService } from './infrastructure/prisma.service';
import { TimeConverter } from './infrastructure/time-converter';
import { TimezoneFinder } from './infrastructure/timezone-finder';
import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
import { DeleteAdGrpcController } from './interface/grpc-controllers/delete-ad.grpc.controller';
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller';
import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
import { UpdateAdGrpcController } from './interface/grpc-controllers/update-ad.grpc.controller';
import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler';
import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler';
import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler';
const grpcControllers = [
CreateAdGrpcController,
UpdateAdGrpcController,
DeleteAdGrpcController,
FindAdByIdGrpcController,
FindAdsByIdsGrpcController,
FindAdsByUserIdGrpcController,
];
const messageHandlers = [
MatcherAdCreatedMessageHandler,
MatcherAdCreationFailedMessageHandler,
UserDeletedMessageHandler,
];
const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
const eventHandlers: Provider[] = [
PublishMessageWhenAdIsCreatedDomainEventHandler,
PublishMessageWhenAdIsUpdatedDomainEventHandler,
PublishMessageWhenAdIsDeletedDomainEventHandler,
];
const commandHandlers: Provider[] = [
CreateAdService,
UpdateAdService,
DeleteAdService,
DeleteUserAdsService,
ValidateAdService,
InvalidateAdService,
];
const commandHandlers: Provider[] = [CreateAdService];
const queryHandlers: Provider[] = [
FindAdByIdQueryHandler,
FindAdsByIdsQueryHandler,
FindAdsByUserIdQueryHandler,
];
const queryHandlers: Provider[] = [FindAdByIdQueryHandler];
const mappers: Provider[] = [AdMapper];
@ -114,7 +72,6 @@ const adapters: Provider[] = [
imports: [CqrsModule],
controllers: [...grpcControllers],
providers: [
...messageHandlers,
...eventHandlers,
...commandHandlers,
...queryHandlers,

View File

@ -1,7 +1,7 @@
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 { 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';
export class CreateAdCommand extends Command {
readonly userId: string;
@ -10,12 +10,11 @@ export class CreateAdCommand extends Command {
readonly frequency: Frequency;
readonly fromDate: string;
readonly toDate: string;
readonly schedule: ScheduleItemProps[];
readonly schedule: ScheduleItem[];
readonly seatsProposed?: number;
readonly seatsRequested?: number;
readonly strict: boolean;
readonly waypoints: Waypoint[];
readonly comment?: string;
constructor(props: CommandProps<CreateAdCommand>) {
super(props);
@ -30,6 +29,5 @@ export class CreateAdCommand extends Command {
this.seatsRequested = props.seatsRequested;
this.strict = props.strict;
this.waypoints = props.waypoints;
this.comment = props.comment;
}
}

View File

@ -1,29 +1,34 @@
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateAdCommand } from './create-ad.command';
import { Inject } from '@nestjs/common';
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
} from '@modules/ad/ad.di-tokens';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
import { Waypoint } from '../../types/waypoint';
import { CreateAdCommand } from './create-ad.command';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
import { ScheduleItem } from '../../types/schedule-item';
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
export function createPropsFromCommand(
command: CreateAdCommand,
datetimeTransformer: DateTimeTransformerPort,
) {
return {
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {}
async execute(command: CreateAdCommand): Promise<AggregateID> {
const ad = AdEntity.create({
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(
fromDate: this.datetimeTransformer.fromDate(
{
date: command.fromDate,
time: command.schedule[0].time,
@ -34,7 +39,7 @@ export function createPropsFromCommand(
},
command.frequency,
),
toDate: datetimeTransformer.toDate(
toDate: this.datetimeTransformer.toDate(
command.toDate,
{
date: command.fromDate,
@ -46,8 +51,8 @@ export function createPropsFromCommand(
},
command.frequency,
),
schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({
day: datetimeTransformer.day(
schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
day: this.datetimeTransformer.day(
scheduleItem.day,
{
date: command.fromDate,
@ -59,7 +64,7 @@ export function createPropsFromCommand(
},
command.frequency,
),
time: datetimeTransformer.time(
time: this.datetimeTransformer.time(
{
date: command.fromDate,
time: scheduleItem.time,
@ -90,23 +95,7 @@ export function createPropsFromCommand(
},
},
})),
comment: command.comment,
};
}
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {}
async execute(command: CreateAdCommand): Promise<AggregateID> {
const ad = AdEntity.create(
createPropsFromCommand(command, this.datetimeTransformer),
);
});
try {
await this.repository.insert(ad);

View File

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

View File

@ -1,18 +0,0 @@
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);
}
}

View File

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

View File

@ -1,29 +0,0 @@
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 })),
),
);
}
}

View File

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

View File

@ -1,25 +0,0 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { InvalidateAdCommand } from './invalidate-ad.command';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { Inject } from '@nestjs/common';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
@CommandHandler(InvalidateAdCommand)
export class InvalidateAdService implements ICommandHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
) {}
async execute(command: InvalidateAdCommand): Promise<AggregateID> {
const ad: AdEntity = await this.repository.findOneById(command.id, {
waypoints: true,
schedule: true,
});
ad.invalid();
await this.repository.update(ad.id, ad);
return ad.id;
}
}

View File

@ -1,16 +0,0 @@
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;
}
}

View File

@ -1,36 +0,0 @@
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),
);
}
}

View File

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

View File

@ -1,25 +0,0 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { ValidateAdCommand } from './validate-ad.command';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { Inject } from '@nestjs/common';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
@CommandHandler(ValidateAdCommand)
export class ValidateAdService implements ICommandHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
) {}
async execute(command: ValidateAdCommand): Promise<AggregateID> {
const ad: AdEntity = await this.repository.findOneById(command.id, {
waypoints: false,
schedule: false,
});
ad.valid();
await this.repository.update(ad.id, ad);
return ad.id;
}
}

View File

@ -1,22 +0,0 @@
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),
);
}
}

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-event';
import { AdCreatedDomainEvent } from '../../domain/events/ad-created.domain-events';
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { AD_CREATED_ROUTING_KEY } from '@src/app.constants';

View File

@ -1,44 +0,0 @@
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),
),
),
);
}
}

View File

@ -1,20 +0,0 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { Inject } from '@nestjs/common';
import { AdEntity } from '../../../domain/ad.entity';
import { FindAdsByIdsQuery } from './find-ads-by-ids.query';
@QueryHandler(FindAdsByIdsQuery)
export class FindAdsByIdsQueryHandler implements IQueryHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
) {}
async execute(query: FindAdsByIdsQuery): Promise<AdEntity[]> {
return await this.repository.findAllByIds(query.ids, {
waypoints: true,
schedule: true,
});
}
}

View File

@ -1,10 +0,0 @@
import { QueryBase } from '@mobicoop/ddd-library';
export class FindAdsByIdsQuery extends QueryBase {
readonly ids: string[];
constructor(ids: string[]) {
super();
this.ids = ids;
}
}

View File

@ -1,25 +0,0 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { Inject } from '@nestjs/common';
import { AdEntity } from '../../../domain/ad.entity';
import { FindAdsByUserIdQuery } from './find-ads-by-user-id.query';
@QueryHandler(FindAdsByUserIdQuery)
export class FindAdsByUserIdQueryHandler implements IQueryHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
) {}
async execute(query: FindAdsByUserIdQuery): Promise<AdEntity[]> {
return await this.repository.findAll(
{
userUuid: query.userId,
},
{
waypoints: true,
schedule: true,
},
);
}
}

View File

@ -1,10 +0,0 @@
import { QueryBase } from '@mobicoop/ddd-library';
export class FindAdsByUserIdQuery extends QueryBase {
readonly userId: string;
constructor(userId: string) {
super();
this.userId = userId;
}
}

View File

@ -0,0 +1,5 @@
export type ScheduleItem = {
day: number;
time: string;
margin: number;
};

View File

@ -1,6 +1,5 @@
import { Address } from './address';
//TODO Why not use the Waypoint value-object from the domain?
export type Waypoint = {
position: number;
} & Address;

View File

@ -1,11 +1,7 @@
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid';
import { AdProps, CreateAdProps, Status } from './ad.types';
import { AdCreatedDomainEvent } from './events/ad-created.domain-event';
import { AdDeletedDomainEvent } from './events/ad-delete.domain-event';
import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event';
import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event';
import { AdValidatedDomainEvent } from './events/ad-validated.domain-event';
import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
import { AdProps, CreateAdProps } from './ad.types';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { WaypointProps } from './value-objects/waypoint.value-object';
@ -14,14 +10,10 @@ export class AdEntity extends AggregateRoot<AdProps> {
static create = (create: CreateAdProps): AdEntity => {
const id = v4();
const props: AdProps = { ...create, status: Status.PENDING };
const props: AdProps = { ...create };
const ad = new AdEntity({ id, props });
ad.addEvent(
new AdCreatedDomainEvent({
metadata: {
correlationId: id,
timestamp: Date.now(),
},
aggregateId: id,
userId: props.userId,
driver: props.driver,
@ -30,9 +22,9 @@ export class AdEntity extends AggregateRoot<AdProps> {
fromDate: props.fromDate,
toDate: props.toDate,
schedule: props.schedule.map((day: ScheduleItemProps) => ({
day: day.day,
day: day.day as number,
time: day.time,
margin: day.margin,
margin: day.margin as number,
})),
seatsProposed: props.seatsProposed,
seatsRequested: props.seatsRequested,
@ -48,80 +40,11 @@ export class AdEntity extends AggregateRoot<AdProps> {
lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat,
})),
comment: props.comment,
}),
);
return ad;
};
valid = (): AdEntity => {
this.props.status = Status.VALID;
this.addEvent(
new AdValidatedDomainEvent({
metadata: {
correlationId: this.id,
timestamp: Date.now(),
},
aggregateId: this.id,
}),
);
return this;
};
invalid = (): AdEntity => {
this.props.status = Status.INVALID;
this.addEvent(
new AdInvalidatedDomainEvent({
metadata: {
correlationId: this.id,
timestamp: Date.now(),
},
aggregateId: this.id,
}),
);
return this;
};
suspend = (): AdEntity => {
this.props.status = Status.SUSPENDED;
this.addEvent(
new AdSuspendedDomainEvent({
metadata: {
correlationId: this.id,
timestamp: Date.now(),
},
aggregateId: this.id,
}),
);
return this;
};
update = (newProps: CreateAdProps): AdEntity => {
this.props.driver = newProps.driver;
this.props.passenger = newProps.passenger;
this.props.frequency = newProps.frequency;
this.props.fromDate = newProps.fromDate;
this.props.toDate = newProps.toDate;
this.props.seatsProposed = newProps.seatsProposed;
this.props.seatsRequested = newProps.seatsRequested;
this.props.strict = newProps.strict;
this.props.comment = newProps.comment;
this.props.schedule = newProps.schedule.map((item) => ({ ...item }));
this.props.waypoints = newProps.waypoints.map((wp) => ({ ...wp }));
//The ad goes back to pending status until it is validated again
this.props.status = Status.PENDING;
this.validate();
return this;
};
delete(): void {
this.addEvent(
new AdDeletedDomainEvent({
aggregateId: this.id,
}),
);
}
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}

View File

@ -1,6 +1,21 @@
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { WaypointProps } from './value-objects/waypoint.value-object';
// All properties that an Ad has
export interface AdProps {
userId: string;
driver: boolean;
passenger: boolean;
frequency: Frequency;
fromDate: string;
toDate: string;
schedule: ScheduleItemProps[];
seatsProposed: number;
seatsRequested: number;
strict: boolean;
waypoints: WaypointProps[];
}
// Properties that are needed for an Ad creation
export interface CreateAdProps {
userId: string;
@ -14,22 +29,9 @@ export interface CreateAdProps {
seatsRequested: number;
strict: boolean;
waypoints: WaypointProps[];
comment?: string;
}
// All properties that an Ad has
export interface AdProps extends CreateAdProps {
status: Status;
}
export enum Frequency {
PUNCTUAL = 'PUNCTUAL',
RECURRENT = 'RECURRENT',
}
export enum Status {
PENDING = 'PENDING',
VALID = 'VALID',
INVALID = 'INVALID',
SUSPENDED = 'SUSPENDED',
}

View File

@ -1,5 +1,4 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
import { ScheduleItemProps } from '../value-objects/schedule-item.value-object';
export class AdCreatedDomainEvent extends DomainEvent {
readonly userId: string;
@ -8,12 +7,11 @@ export class AdCreatedDomainEvent extends DomainEvent {
readonly frequency: string;
readonly fromDate: string;
readonly toDate: string;
readonly schedule: ScheduleItemProps[];
readonly schedule: ScheduleItem[];
readonly seatsProposed: number;
readonly seatsRequested: number;
readonly strict: boolean;
readonly waypoints: Waypoint[];
readonly comment?: string;
constructor(props: DomainEventProps<AdCreatedDomainEvent>) {
super(props);
@ -28,10 +26,15 @@ export class AdCreatedDomainEvent extends DomainEvent {
this.seatsRequested = props.seatsRequested;
this.strict = props.strict;
this.waypoints = props.waypoints;
this.comment = props.comment;
}
}
export class ScheduleItem {
day: number;
time: string;
margin: number;
}
export class Waypoint {
position: number;
name?: string;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +0,0 @@
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);
}
}

View File

@ -6,13 +6,13 @@ import { ValueObject } from '@mobicoop/ddd-library';
* */
export interface ScheduleItemProps {
day: number;
day?: number;
time: string;
margin: number;
margin?: number;
}
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
get day(): number {
get day(): number | undefined {
return this.props.day;
}
@ -20,7 +20,7 @@ export class ScheduleItem extends ValueObject<ScheduleItemProps> {
return this.props.time;
}
get margin(): number {
get margin(): number | undefined {
return this.props.margin;
}

View File

@ -1,58 +1,44 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AdEntity } from '../core/domain/ad.entity';
import { AdMapper } from '../ad.mapper';
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
import {
LoggerBase,
MessagePublisherPort,
PrismaRepositoryBase,
} from '@mobicoop/ddd-library';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { SERVICE_NAME } from '@src/app.constants';
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
import { AdMapper } from '../ad.mapper';
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
import { AdEntity } from '../core/domain/ad.entity';
import { PrismaService } from './prisma.service';
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
import { SERVICE_NAME } from '@src/app.constants';
export type AdBaseModel = {
uuid: string;
userUuid: string;
driver: boolean;
passenger: boolean;
status: string;
frequency: string;
fromDate: Date;
toDate: Date;
seatsProposed: number;
seatsRequested: number;
strict: boolean;
comment?: string;
createdAt: Date;
updatedAt: Date;
};
export type AdReadModel = AdBaseModel & {
waypoints: WaypointModel[];
schedule: ScheduleItemModel[];
createdAt: Date;
updatedAt: Date;
};
export type AdWriteModel = AdBaseModel & {
schedule: ScheduleWriteModel | undefined;
waypoints: WaypointWriteModel | undefined;
};
export type ScheduleWriteModel = {
deleteMany?: PastCreatedFilter;
create: ScheduleItemModel[];
};
export type WaypointWriteModel = {
deleteMany?: PastCreatedFilter;
waypoints: {
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 };
};
schedule: {
create: ScheduleItemModel[];
};
};
export type ScheduleItemModel = {

View File

@ -1,11 +1,10 @@
import { ResponseBase } from '@mobicoop/ddd-library';
import { Frequency, Status } from '@modules/ad/core/domain/ad.types';
import { Frequency } from '@modules/ad/core/domain/ad.types';
export class AdResponseDto extends ResponseBase {
userId: string;
driver: boolean;
passenger: boolean;
status: Status;
frequency: Frequency;
fromDate: string;
toDate: string;
@ -28,5 +27,4 @@ export class AdResponseDto extends ResponseBase {
lon: number;
lat: number;
}[];
comment?: string;
}

View File

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

View File

@ -4,10 +4,9 @@ package ad;
service AdService {
rpc FindOneById(AdById) returns (Ad);
rpc FindAllByIds(AdsById) returns (Ads);
rpc FindAllByUserId(UserById) returns (Ads);
rpc FindAll(AdFilter) returns (Ads);
rpc Create(Ad) returns (AdById);
rpc Update(Ad) returns (Empty);
rpc Update(Ad) returns (Ad);
rpc Delete(AdById) returns (Empty);
}
@ -15,14 +14,6 @@ message AdById {
string id = 1;
}
message UserById {
string id = 1;
}
message AdsById {
repeated string ids = 1;
}
message Ad {
string id = 1;
string userId = 2;
@ -36,7 +27,6 @@ message Ad {
int32 seatsRequested = 10;
bool strict = 11;
repeated Waypoint waypoints = 12;
optional string comment = 13;
}
message ScheduleItem {
@ -58,13 +48,18 @@ message Waypoint {
}
enum Frequency {
UNSPECIFIED = 0;
PUNCTUAL = 1;
RECURRENT = 2;
}
message AdFilter {
int32 page = 1;
int32 perPage = 2;
}
message Ads {
repeated Ad ads = 1;
repeated Ad data = 1;
int32 total = 2;
}
message Empty {}

View File

@ -1,45 +0,0 @@
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,
});
}
}
}

View File

@ -3,10 +3,8 @@ import {
IsBoolean,
IsInt,
IsEnum,
IsString,
ValidateNested,
ArrayMinSize,
Length,
IsUUID,
IsArray,
IsISO8601,
@ -80,9 +78,4 @@ export class CreateAdRequestDto {
@HasValidPositionIndexes()
@ValidateNested({ each: true })
waypoints: WaypointDto[];
@Length(0, 2000)
@IsString()
@IsOptional()
comment?: string;
}

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
import { IsUUID } from 'class-validator';
import { CreateAdRequestDto } from './create-ad.request.dto';
export class UpdateAdRequestDto extends CreateAdRequestDto {
@IsUUID(4)
id: string;
}

View File

@ -1,42 +0,0 @@
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { AdMapper } from '@modules/ad/ad.mapper';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { RpcValidationPipe } from '@mobicoop/ddd-library';
import { GRPC_SERVICE_NAME } from '@src/app.constants';
import { FindAdsByIdsRequestDto } from './dtos/find-ads-by-ids.request.dto';
import { AdsResponseDto } from '../dtos/ads.response.dto';
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class FindAdsByIdsGrpcController {
constructor(
protected readonly mapper: AdMapper,
private readonly queryBus: QueryBus,
) {}
@GrpcMethod(GRPC_SERVICE_NAME, 'FindAllByIds')
async findAllByIds(data: FindAdsByIdsRequestDto): Promise<AdsResponseDto> {
try {
const ads: AdEntity[] = await this.queryBus.execute(
new FindAdsByIdsQuery(data.ids),
);
return {
ads: ads.map((ad: AdEntity) => this.mapper.toResponse(ad)),
};
} catch (e) {
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,
message: e.message,
});
}
}
}

View File

@ -1,44 +0,0 @@
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { AdMapper } from '@modules/ad/ad.mapper';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { RpcValidationPipe } from '@mobicoop/ddd-library';
import { GRPC_SERVICE_NAME } from '@src/app.constants';
import { AdsResponseDto } from '../dtos/ads.response.dto';
import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query';
import { FindAdsByUserIdRequestDto } from './dtos/find-ads-by-user-id.request.dto';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class FindAdsByUserIdGrpcController {
constructor(
protected readonly mapper: AdMapper,
private readonly queryBus: QueryBus,
) {}
@GrpcMethod(GRPC_SERVICE_NAME, 'FindAllByUserId')
async findAllByUserId(
data: FindAdsByUserIdRequestDto,
): Promise<AdsResponseDto> {
try {
const ads: AdEntity[] = await this.queryBus.execute(
new FindAdsByUserIdQuery(data.id),
);
return {
ads: ads.map((ad: AdEntity) => this.mapper.toResponse(ad)),
};
} catch (e) {
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,
message: e.message,
});
}
}
}

View File

@ -1,43 +0,0 @@
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;
}
}
}

View File

@ -1,29 +0,0 @@
import { Injectable } from '@nestjs/common';
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { CommandBus } from '@nestjs/cqrs';
import { MATCHER_AD_CREATED_MESSAGE_HANDLER } from '@src/app.constants';
import { MatcherAdCreatedIntegrationEvent } from './matcher-ad.types';
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
@Injectable()
export class MatcherAdCreatedMessageHandler {
constructor(private readonly commandBus: CommandBus) {}
@RabbitSubscribe({
name: MATCHER_AD_CREATED_MESSAGE_HANDLER,
})
public async matcherAdCreated(message: string) {
try {
const matcherAdCreatedIntegrationEvent: MatcherAdCreatedIntegrationEvent =
JSON.parse(message);
await this.commandBus.execute(
new ValidateAdCommand({
id: matcherAdCreatedIntegrationEvent.id,
}),
);
} catch (error: any) {
// do not throw error to acknowledge incoming message
// error handling should be done in the command handler, if relevant
}
}
}

View File

@ -1,29 +0,0 @@
import { Injectable } from '@nestjs/common';
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
import { CommandBus } from '@nestjs/cqrs';
import { MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER } from '@src/app.constants';
import { MatcherAdCreationFailedIntegrationEvent } from './matcher-ad.types';
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
@Injectable()
export class MatcherAdCreationFailedMessageHandler {
constructor(private readonly commandBus: CommandBus) {}
@RabbitSubscribe({
name: MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER,
})
public async matcherAdCreationFailed(message: string) {
try {
const matcherAdCreationFailedIntegrationEvent: MatcherAdCreationFailedIntegrationEvent =
JSON.parse(message);
await this.commandBus.execute(
new InvalidateAdCommand({
id: matcherAdCreationFailedIntegrationEvent.id,
}),
);
} catch (error: any) {
// do not throw error to acknowledge incoming message
// error handling should be done in the command handler, if relevant
}
}
}

View File

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

View File

@ -1,23 +0,0 @@
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 }),
);
}
}

View File

@ -2,7 +2,7 @@ import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency, Status } from '@modules/ad/core/domain/ad.types';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import {
AdReadModel,
AdWriteModel,
@ -17,7 +17,6 @@ const adEntity: AdEntity = new AdEntity({
userId: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
driver: false,
passenger: true,
status: Status.PENDING,
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-21',
toDate: '2023-06-21',
@ -68,7 +67,6 @@ const adReadModel: AdReadModel = {
userUuid: '7ca2490b-d04d-4ac5-8d6c-5c416fab922e',
driver: false,
passenger: true,
status: Status.PENDING,
frequency: Frequency.PUNCTUAL,
fromDate: new Date('2023-06-21'),
toDate: new Date('2023-06-21'),
@ -111,7 +109,6 @@ const adReadModel: AdReadModel = {
strict: false,
seatsProposed: 3,
seatsRequested: 1,
comment: '',
createdAt: now,
updatedAt: now,
};
@ -145,9 +142,9 @@ describe('Ad Mapper', () => {
it('should map domain entity to persistence data', async () => {
const mapped: AdWriteModel = adMapper.toPersistence(adEntity);
expect(mapped.waypoints?.create[0].uuid.length).toBe(36);
expect(mapped.waypoints?.create[1].uuid.length).toBe(36);
expect(mapped.schedule?.create.length).toBe(1);
expect(mapped.waypoints.create[0].uuid.length).toBe(36);
expect(mapped.waypoints.create[1].uuid.length).toBe(36);
expect(mapped.schedule.create.length).toBe(1);
});
it('should map persisted data to domain entity', async () => {

View File

@ -1,9 +1,5 @@
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import {
CreateAdProps,
Frequency,
Status,
} 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';
const originWaypointProps: WaypointProps = {
@ -39,7 +35,6 @@ const baseCreateAdProps = {
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
comment: "J'accepte les chiens mais pas les chats",
};
const punctualCreateAdProps = {
fromDate: '2023-06-21',
@ -129,15 +124,11 @@ describe('Ad entity create', () => {
punctualPassengerCreateAdProps,
);
expect(punctualPassengerAd.id.length).toBe(36);
expect(punctualPassengerAd.getProps().status).toBe(Status.PENDING);
expect(punctualPassengerAd.getProps().schedule.length).toBe(1);
expect(punctualPassengerAd.getProps().schedule[0].day).toBe(3);
expect(punctualPassengerAd.getProps().schedule[0].time).toBe('08:30');
expect(punctualPassengerAd.getProps().driver).toBeFalsy();
expect(punctualPassengerAd.getProps().passenger).toBeTruthy();
expect(punctualPassengerAd.getProps().comment).toBe(
"J'accepte les chiens mais pas les chats",
);
});
it('should create a new punctual driver ad entity', async () => {
const punctualDriverAd: AdEntity = AdEntity.create(
@ -200,33 +191,3 @@ describe('Ad entity create', () => {
});
});
});
describe('Ad entity validate status', () => {
it('should validate status of a pending ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
);
punctualPassengerAd.valid();
expect(punctualPassengerAd.getProps().status).toBe(Status.VALID);
});
});
describe('Ad entity invalidate status', () => {
it('should invalidate status of a pending ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
);
punctualPassengerAd.invalid();
expect(punctualPassengerAd.getProps().status).toBe(Status.INVALID);
});
});
describe('Ad entity suspend status', () => {
it('should suspend status of a pending ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
);
punctualPassengerAd.suspend();
expect(punctualPassengerAd.getProps().status).toBe(Status.SUSPENDED);
});
});

View File

@ -1,17 +1,18 @@
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
import { Test, TestingModule } from '@nestjs/testing';
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
} from '@modules/ad/ad.di-tokens';
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { 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';
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 { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
const originWaypoint: WaypointDto = {
position: 0,
@ -63,6 +64,13 @@ const mockAdRepository = {
}),
};
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
day: jest.fn(),
time: jest.fn(),
};
describe('create-ad.service', () => {
let createAdService: CreateAdService;
@ -75,7 +83,7 @@ describe('create-ad.service', () => {
},
{
provide: INPUT_DATETIME_TRANSFORMER,
useValue: mockInputDateTimeTransformer(),
useValue: mockInputDateTimeTransformer,
},
CreateAdService,
],

View File

@ -1,9 +1,8 @@
import { AggregateID } from '@mobicoop/ddd-library';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
import { FindAdByIdQueryHandler } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Test, TestingModule } from '@nestjs/testing';
@ -45,9 +44,7 @@ const punctualCreateAdProps = {
toDate: '2023-06-22',
schedule: [
{
day: 4,
time: '08:30',
margin: 900,
},
],
frequency: Frequency.PUNCTUAL,
@ -63,11 +60,10 @@ const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
const mockAdRepository = {
findOneById: jest.fn().mockImplementation(() => ad),
update: jest.fn().mockImplementation(() => ad.id),
};
describe('Validate Ad Service', () => {
let validateAdService: ValidateAdService;
describe('find-ad-by-id.query-handler', () => {
let findAdByIdQueryHandler: FindAdByIdQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -76,25 +72,27 @@ describe('Validate Ad Service', () => {
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
ValidateAdService,
FindAdByIdQueryHandler,
],
}).compile();
validateAdService = module.get<ValidateAdService>(ValidateAdService);
findAdByIdQueryHandler = module.get<FindAdByIdQueryHandler>(
FindAdByIdQueryHandler,
);
});
it('should be defined', () => {
expect(validateAdService).toBeDefined();
expect(findAdByIdQueryHandler).toBeDefined();
});
describe('execution', () => {
it('should validate an ad', async () => {
jest.spyOn(ad, 'valid');
const validateAdCommand = new ValidateAdCommand(ad.id);
const result: AggregateID =
await validateAdService.execute(validateAdCommand);
expect(result).toBe(ad.id);
expect(ad.valid).toHaveBeenCalledTimes(1);
it('should return an ad', async () => {
const findAdbyIdQuery = new FindAdByIdQuery(
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
);
const ad: AdEntity =
await findAdByIdQueryHandler.execute(findAdbyIdQuery);
expect(ad.getProps().fromDate).toBe('2023-06-22');
});
});
});

View File

@ -1,6 +1,6 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-event';
import { AdCreatedDomainEvent } from '@modules/ad/core/domain/events/ad-created.domain-events';
import { Test, TestingModule } from '@nestjs/testing';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { AD_CREATED_ROUTING_KEY } from '@src/app.constants';

View File

@ -1,10 +1,51 @@
import { IdResponse, RpcExceptionCode } from '@mobicoop/ddd-library';
import { IdResponse } from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller';
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { CommandBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing';
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 = {
execute: jest
@ -48,7 +89,7 @@ describe('Create Ad Grpc Controller', () => {
it('should create a new ad', async () => {
jest.spyOn(mockCommandBus, 'execute');
const result: IdResponse = await createAdGrpcController.create(
punctualCreateAdRequest(),
punctualCreateAdRequest,
);
expect(result).toBeInstanceOf(IdResponse);
expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
@ -59,7 +100,7 @@ describe('Create Ad Grpc Controller', () => {
jest.spyOn(mockCommandBus, 'execute');
expect.assertions(3);
try {
await createAdGrpcController.create(punctualCreateAdRequest());
await createAdGrpcController.create(punctualCreateAdRequest);
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
@ -71,7 +112,7 @@ describe('Create Ad Grpc Controller', () => {
jest.spyOn(mockCommandBus, 'execute');
expect.assertions(3);
try {
await createAdGrpcController.create(punctualCreateAdRequest());
await createAdGrpcController.create(punctualCreateAdRequest);
} catch (e: any) {
expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);

View File

@ -1,24 +1,13 @@
import { Module, Provider } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SERVICE_NAME } from '@src/app.constants';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
MessageBrokerPublisher,
} from '@mobicoop/message-broker-module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
MATCHER_AD_CREATED_MESSAGE_HANDLER,
MATCHER_AD_CREATED_QUEUE,
MATCHER_AD_CREATED_ROUTING_KEY,
MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER,
MATCHER_AD_CREATION_FAILED_QUEUE,
MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
SERVICE_NAME,
USER_DELETED_MESSAGE_HANDLER,
USER_DELETED_QUEUE,
USER_DELETED_ROUTING_KEY,
} from '@src/app.constants';
const imports = [
MessageBrokerModule.forRootAsync({
@ -35,20 +24,6 @@ const imports = [
) as boolean,
},
name: SERVICE_NAME,
handlers: {
[MATCHER_AD_CREATED_MESSAGE_HANDLER]: {
routingKey: MATCHER_AD_CREATED_ROUTING_KEY,
queue: MATCHER_AD_CREATED_QUEUE,
},
[MATCHER_AD_CREATION_FAILED_MESSAGE_HANDLER]: {
routingKey: MATCHER_AD_CREATION_FAILED_ROUTING_KEY,
queue: MATCHER_AD_CREATION_FAILED_QUEUE,
},
[USER_DELETED_MESSAGE_HANDLER]: {
routingKey: USER_DELETED_ROUTING_KEY,
queue: USER_DELETED_QUEUE,
},
},
}),
}),
];

View File

@ -1,10 +0,0 @@
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(),
};
}

View File

@ -1,57 +0,0 @@
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,
};
}

View File

@ -1,42 +0,0 @@
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);
});
});

View File

@ -1,53 +0,0 @@
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);
});
});

View File

@ -1,47 +0,0 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
import { FindAdByIdQueryHandler } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Test, TestingModule } from '@nestjs/testing';
import { punctualPassengerCreateAdProps } from './ad.fixtures';
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps());
const mockAdRepository = {
findOneById: jest.fn().mockImplementation(() => ad),
};
describe('find-ad-by-id.query-handler', () => {
let findAdByIdQueryHandler: FindAdByIdQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
FindAdByIdQueryHandler,
],
}).compile();
findAdByIdQueryHandler = module.get<FindAdByIdQueryHandler>(
FindAdByIdQueryHandler,
);
});
it('should be defined', () => {
expect(findAdByIdQueryHandler).toBeDefined();
});
describe('execution', () => {
it('should return an ad', async () => {
const findAdbyIdQuery = new FindAdByIdQuery(
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
);
const ad: AdEntity =
await findAdByIdQueryHandler.execute(findAdbyIdQuery);
expect(ad.getProps().fromDate).toBe('2023-06-22');
});
});
});

View File

@ -1,107 +0,0 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Test, TestingModule } from '@nestjs/testing';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: [
{
day: 4,
time: '08:30',
margin: 900,
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const ads: AdEntity[] = [
AdEntity.create(punctualPassengerCreateAdProps),
AdEntity.create(punctualPassengerCreateAdProps),
AdEntity.create(punctualPassengerCreateAdProps),
];
const mockAdRepository = {
findAllByIds: jest.fn().mockImplementation(() => ads),
};
describe('Find Ads By Ids Query Handler', () => {
let findAdsByIdsQueryHandler: FindAdsByIdsQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
FindAdsByIdsQueryHandler,
],
}).compile();
findAdsByIdsQueryHandler = module.get<FindAdsByIdsQueryHandler>(
FindAdsByIdsQueryHandler,
);
});
it('should be defined', () => {
expect(findAdsByIdsQueryHandler).toBeDefined();
});
describe('execution', () => {
it('should return an ad', async () => {
const findAdsByIdsQuery = new FindAdsByIdsQuery([
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
'dd264806-13b4-4226-9b18-87adf0ad5dd2',
'dd264806-13b4-4226-9b18-87adf0ad5dd3',
]);
const ads: AdEntity[] =
await findAdsByIdsQueryHandler.execute(findAdsByIdsQuery);
expect(ads).toHaveLength(3);
expect(ads[1].getProps().fromDate).toBe('2023-06-22');
});
});
});

View File

@ -1,105 +0,0 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query';
import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Test, TestingModule } from '@nestjs/testing';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: [
{
day: 4,
time: '08:30',
margin: 900,
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const ads: AdEntity[] = [
AdEntity.create(punctualPassengerCreateAdProps),
AdEntity.create(punctualPassengerCreateAdProps),
AdEntity.create(punctualPassengerCreateAdProps),
];
const mockAdRepository = {
findAll: jest.fn().mockImplementation(() => ads),
};
describe('Find Ads By User Id Query Handler', () => {
let findAdsByUserIdQueryHandler: FindAdsByUserIdQueryHandler;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
FindAdsByUserIdQueryHandler,
],
}).compile();
findAdsByUserIdQueryHandler = module.get<FindAdsByUserIdQueryHandler>(
FindAdsByUserIdQueryHandler,
);
});
it('should be defined', () => {
expect(findAdsByUserIdQueryHandler).toBeDefined();
});
describe('execution', () => {
it('should return an ad', async () => {
const findAdsByIdsQuery = new FindAdsByUserIdQuery(
'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
);
const ads: AdEntity[] =
await findAdsByUserIdQueryHandler.execute(findAdsByIdsQuery);
expect(ads).toHaveLength(3);
expect(ads[1].getProps().fromDate).toBe('2023-06-22');
});
});
});

View File

@ -1,100 +0,0 @@
import { AggregateID } from '@mobicoop/ddd-library';
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
import { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Test, TestingModule } from '@nestjs/testing';
const originWaypointProps: WaypointProps = {
position: 0,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
};
const destinationWaypointProps: WaypointProps = {
position: 1,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
};
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: [
{
day: 4,
time: '08:30',
margin: 900,
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
const mockAdRepository = {
findOneById: jest.fn().mockImplementation(() => ad),
update: jest.fn().mockImplementation(() => ad.id),
};
describe('Invalidate Ad Service', () => {
let invalidateAdService: InvalidateAdService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
InvalidateAdService,
],
}).compile();
invalidateAdService = module.get<InvalidateAdService>(InvalidateAdService);
});
it('should be defined', () => {
expect(invalidateAdService).toBeDefined();
});
describe('execution', () => {
it('should invalidate an ad', async () => {
jest.spyOn(ad, 'invalid');
const invalidateAdCommand = new InvalidateAdCommand(ad.id);
const result: AggregateID =
await invalidateAdService.execute(invalidateAdCommand);
expect(result).toBe(ad.id);
expect(ad.invalid).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -1,61 +0,0 @@
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);
});
});

View File

@ -1,71 +0,0 @@
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();
});
});
});

View File

@ -1,43 +0,0 @@
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],
};
}

Some files were not shown because too many files have changed in this diff Show More