Merge branch 'DDH' into 'main'
DDH : ad creation See merge request v3/service/matcher!10
This commit is contained in:
commit
bca3374255
32
.env.dist
32
.env.dist
|
@ -4,12 +4,25 @@ SERVICE_PORT=5005
|
||||||
SERVICE_CONFIGURATION_DOMAIN=MATCHER
|
SERVICE_CONFIGURATION_DOMAIN=MATCHER
|
||||||
HEALTH_SERVICE_PORT=6005
|
HEALTH_SERVICE_PORT=6005
|
||||||
|
|
||||||
|
# PRISMA
|
||||||
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
|
||||||
|
|
||||||
|
# MESSAGE BROKER
|
||||||
|
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||||
|
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
|
|
||||||
|
# REDIS
|
||||||
|
REDIS_HOST=v3-redis
|
||||||
|
REDIS_PASSWORD=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
# CACHE
|
||||||
|
CACHE_TTL=5000
|
||||||
|
|
||||||
# DEFAULT CONFIGURATION
|
# DEFAULT CONFIGURATION
|
||||||
|
|
||||||
# default identifier used for match requests
|
# default identifier used for match requests
|
||||||
DEFAULT_UUID=00000000-0000-0000-0000-000000000000
|
DEFAULT_UUID=00000000-0000-0000-0000-000000000000
|
||||||
# default timezone
|
|
||||||
DEFAULT_TIMEZONE=Europe/Paris
|
|
||||||
# default number of seats proposed as driver
|
# default number of seats proposed as driver
|
||||||
DEFAULT_SEATS=3
|
DEFAULT_SEATS=3
|
||||||
# algorithm type
|
# algorithm type
|
||||||
|
@ -41,18 +54,3 @@ MAX_DETOUR_DURATION_RATIO=0.3
|
||||||
GEOROUTER_TYPE=graphhopper
|
GEOROUTER_TYPE=graphhopper
|
||||||
# georouter url
|
# georouter url
|
||||||
GEOROUTER_URL=http://localhost:8989
|
GEOROUTER_URL=http://localhost:8989
|
||||||
|
|
||||||
# PRISMA
|
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"
|
|
||||||
|
|
||||||
# RABBIT MQ
|
|
||||||
RMQ_URI=amqp://v3-broker:5672
|
|
||||||
RMQ_EXCHANGE=mobicoop
|
|
||||||
|
|
||||||
# REDIS
|
|
||||||
REDIS_HOST=v3-redis
|
|
||||||
REDIS_PASSWORD=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# CACHE
|
|
||||||
CACHE_TTL=5000
|
|
||||||
|
|
34
ci/.env.ci
34
ci/.env.ci
|
@ -2,12 +2,27 @@
|
||||||
SERVICE_URL=0.0.0.0
|
SERVICE_URL=0.0.0.0
|
||||||
SERVICE_PORT=5005
|
SERVICE_PORT=5005
|
||||||
|
|
||||||
|
# PRISMA
|
||||||
|
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
||||||
|
|
||||||
|
# MESSAGE BROKER
|
||||||
|
MESSAGE_BROKER_URI=amqp://v3-broker:5672
|
||||||
|
MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
|
|
||||||
|
# REDIS
|
||||||
|
REDIS_HOST=v3-redis
|
||||||
|
REDIS_PASSWORD=redis
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
# IMAGES
|
||||||
|
BROKER_IMAGE=rabbitmq:3-alpine
|
||||||
|
REDIS_IMAGE=redis:7.0-alpine
|
||||||
|
POSTGRES_IMAGE=postgis/postgis:15-3.3
|
||||||
|
|
||||||
# DEFAULT CONFIGURATION
|
# DEFAULT CONFIGURATION
|
||||||
|
|
||||||
# default identifier used for match requests
|
# default identifier used for match requests
|
||||||
DEFAULT_UUID=00000000-0000-0000-0000-000000000000
|
DEFAULT_UUID=00000000-0000-0000-0000-000000000000
|
||||||
# default timezone
|
|
||||||
DEFAULT_TIMEZONE=Europe/Paris
|
|
||||||
# default number of seats proposed as driver
|
# default number of seats proposed as driver
|
||||||
DEFAULT_SEATS=3
|
DEFAULT_SEATS=3
|
||||||
# algorithm type
|
# algorithm type
|
||||||
|
@ -41,19 +56,4 @@ GEOROUTER_TYPE=graphhopper
|
||||||
GEOROUTER_URL=http://localhost:8989
|
GEOROUTER_URL=http://localhost:8989
|
||||||
|
|
||||||
|
|
||||||
# PRISMA
|
|
||||||
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=public"
|
|
||||||
|
|
||||||
# RABBIT MQ
|
|
||||||
RMQ_URI=amqp://v3-broker:5672
|
|
||||||
|
|
||||||
# REDIS
|
|
||||||
REDIS_IMAGE=redis:7.0-alpine
|
|
||||||
REDIS_HOST=v3-redis
|
|
||||||
REDIS_PASSWORD=redis
|
|
||||||
|
|
||||||
# MESSAGE BROKER
|
|
||||||
BROKER_IMAGE=rabbitmq:3-alpine
|
|
||||||
|
|
||||||
# POSTGRES
|
|
||||||
POSTGRES_IMAGE=postgis/postgis:15-3.3
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mobicoop/matcher",
|
"name": "@mobicoop/matcher",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"description": "Mobicoop V3 Matcher",
|
"description": "Mobicoop V3 Matcher",
|
||||||
"author": "sbriat",
|
"author": "sbriat",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -17,39 +17,37 @@
|
||||||
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix-dry-run --ignore-path .gitignore",
|
||||||
"pretty:check": "./node_modules/.bin/prettier --check .",
|
"pretty:check": "./node_modules/.bin/prettier --check .",
|
||||||
"pretty": "./node_modules/.bin/prettier --write .",
|
"pretty": "./node_modules/.bin/prettier --write .",
|
||||||
"test": "npm run migrate:test && dotenv -e .env.test jest",
|
"test": "npm run test:unit && npm run test:integration",
|
||||||
"test:unit": "jest --testPathPattern 'tests/unit/' --verbose",
|
"test:unit": "jest --testPathPattern 'tests/unit/' --verbose",
|
||||||
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||||
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose",
|
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
|
||||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --verbose",
|
"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:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||||
"test:cov:watch": "jest --testPathPattern 'tests/unit/' --coverage --watch",
|
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"generate": "docker exec v3-matcher-api sh -c 'npx prisma generate'",
|
|
||||||
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
|
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
|
||||||
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
|
||||||
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
|
||||||
"migrate:deploy": "npx prisma migrate deploy"
|
"migrate:deploy": "npx prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automapper/classes": "^8.7.7",
|
"@grpc/grpc-js": "^1.8.14",
|
||||||
"@automapper/core": "^8.7.7",
|
|
||||||
"@automapper/nestjs": "^8.7.7",
|
|
||||||
"@golevelup/nestjs-rabbitmq": "^3.6.0",
|
|
||||||
"@grpc/grpc-js": "^1.8.13",
|
|
||||||
"@grpc/proto-loader": "^0.7.6",
|
"@grpc/proto-loader": "^0.7.6",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/configuration-module": "^1.0.0",
|
"@mobicoop/configuration-module": "^1.2.0",
|
||||||
|
"@mobicoop/ddd-library": "^1.1.0",
|
||||||
|
"@mobicoop/health-module": "^2.0.0",
|
||||||
|
"@mobicoop/message-broker-module": "^1.2.0",
|
||||||
"@nestjs/axios": "^2.0.0",
|
"@nestjs/axios": "^2.0.0",
|
||||||
"@nestjs/cache-manager": "^1.0.0",
|
"@nestjs/cache-manager": "^1.0.0",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.3.1",
|
"@nestjs/config": "^2.3.1",
|
||||||
"@nestjs/core": "^9.0.0",
|
"@nestjs/core": "^9.0.0",
|
||||||
"@nestjs/cqrs": "^9.0.3",
|
"@nestjs/cqrs": "^9.0.3",
|
||||||
|
"@nestjs/event-emitter": "^1.4.2",
|
||||||
"@nestjs/microservices": "^9.4.0",
|
"@nestjs/microservices": "^9.4.0",
|
||||||
"@nestjs/platform-express": "^9.0.0",
|
"@nestjs/platform-express": "^9.0.0",
|
||||||
"@nestjs/terminus": "^9.2.2",
|
"@nestjs/terminus": "^9.2.2",
|
||||||
"@prisma/client": "^4.12.0",
|
"@prisma/client": "^4.13.0",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"cache-manager": "^5.2.0",
|
"cache-manager": "^5.2.0",
|
||||||
"cache-manager-ioredis-yet": "^1.1.0",
|
"cache-manager-ioredis-yet": "^1.1.0",
|
||||||
|
@ -58,7 +56,8 @@
|
||||||
"geo-tz": "^7.0.7",
|
"geo-tz": "^7.0.7",
|
||||||
"geographiclib-geodesic": "^2.0.0",
|
"geographiclib-geodesic": "^2.0.0",
|
||||||
"got": "^11.8.6",
|
"got": "^11.8.6",
|
||||||
"ioredis": "^5.3.1",
|
"ioredis": "^5.3.2",
|
||||||
|
"nestjs-request-context": "^2.1.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
"timezonecomplete": "^5.12.4"
|
"timezonecomplete": "^5.12.4"
|
||||||
|
@ -71,6 +70,7 @@
|
||||||
"@types/jest": "29.5.0",
|
"@types/jest": "29.5.0",
|
||||||
"@types/node": "18.15.11",
|
"@types/node": "18.15.11",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"dotenv-cli": "^7.2.1",
|
"dotenv-cli": "^7.2.1",
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"prisma": "^4.12.0",
|
"prisma": "^4.13.0",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "29.0.5",
|
"ts-jest": "29.0.5",
|
||||||
|
@ -95,15 +95,13 @@
|
||||||
"ts"
|
"ts"
|
||||||
],
|
],
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
".controller.ts",
|
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
".di-tokens.ts",
|
||||||
".profile.ts",
|
".response.ts",
|
||||||
".exception.ts",
|
".port.ts",
|
||||||
".enum.ts",
|
"prisma.service.ts",
|
||||||
"main.ts",
|
"main.ts"
|
||||||
"prisma-service.ts"
|
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
@ -114,17 +112,19 @@
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
".controller.ts",
|
|
||||||
".module.ts",
|
".module.ts",
|
||||||
".request.ts",
|
".dto.ts",
|
||||||
".presenter.ts",
|
".di-tokens.ts",
|
||||||
".profile.ts",
|
".response.ts",
|
||||||
".exception.ts",
|
".port.ts",
|
||||||
".enum.ts",
|
"prisma.service.ts",
|
||||||
"main.ts",
|
"main.ts"
|
||||||
"prisma-service.ts"
|
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@modules(.*)": "<rootDir>/modules/$1",
|
||||||
|
"^@src(.*)": "<rootDir>$1"
|
||||||
|
},
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,26 +11,14 @@ CREATE TYPE "Frequency" AS ENUM ('PUNCTUAL', 'RECURRENT');
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "ad" (
|
CREATE TABLE "ad" (
|
||||||
"uuid" UUID NOT NULL,
|
"uuid" UUID NOT NULL,
|
||||||
"userUuid" UUID NOT NULL,
|
|
||||||
"driver" BOOLEAN NOT NULL,
|
"driver" BOOLEAN NOT NULL,
|
||||||
"passenger" BOOLEAN NOT NULL,
|
"passenger" BOOLEAN NOT NULL,
|
||||||
"frequency" "Frequency" NOT NULL,
|
"frequency" "Frequency" NOT NULL,
|
||||||
"fromDate" DATE NOT NULL,
|
"fromDate" DATE NOT NULL,
|
||||||
"toDate" DATE NOT NULL,
|
"toDate" DATE NOT NULL,
|
||||||
"monTime" TIMESTAMPTZ,
|
"seatsProposed" SMALLINT NOT NULL,
|
||||||
"tueTime" TIMESTAMPTZ,
|
"seatsRequested" SMALLINT NOT NULL,
|
||||||
"wedTime" TIMESTAMPTZ,
|
"strict" BOOLEAN NOT NULL,
|
||||||
"thuTime" TIMESTAMPTZ,
|
|
||||||
"friTime" TIMESTAMPTZ,
|
|
||||||
"satTime" TIMESTAMPTZ,
|
|
||||||
"sunTime" TIMESTAMPTZ,
|
|
||||||
"monMargin" INTEGER NOT NULL,
|
|
||||||
"tueMargin" INTEGER NOT NULL,
|
|
||||||
"wedMargin" INTEGER NOT NULL,
|
|
||||||
"thuMargin" INTEGER NOT NULL,
|
|
||||||
"friMargin" INTEGER NOT NULL,
|
|
||||||
"satMargin" INTEGER NOT NULL,
|
|
||||||
"sunMargin" INTEGER NOT NULL,
|
|
||||||
"driverDuration" INTEGER,
|
"driverDuration" INTEGER,
|
||||||
"driverDistance" INTEGER,
|
"driverDistance" INTEGER,
|
||||||
"passengerDuration" INTEGER,
|
"passengerDuration" INTEGER,
|
||||||
|
@ -39,16 +27,25 @@ CREATE TABLE "ad" (
|
||||||
"direction" geography(LINESTRING),
|
"direction" geography(LINESTRING),
|
||||||
"fwdAzimuth" INTEGER NOT NULL,
|
"fwdAzimuth" INTEGER NOT NULL,
|
||||||
"backAzimuth" INTEGER NOT NULL,
|
"backAzimuth" INTEGER NOT NULL,
|
||||||
"seatsDriver" SMALLINT NOT NULL,
|
|
||||||
"seatsPassenger" SMALLINT NOT NULL,
|
|
||||||
"seatsUsed" SMALLINT NOT NULL,
|
|
||||||
"strict" BOOLEAN NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid")
|
CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "schedule_item" (
|
||||||
|
"uuid" UUID NOT NULL,
|
||||||
|
"adUuid" UUID NOT NULL,
|
||||||
|
"day" INTEGER NOT NULL,
|
||||||
|
"time" TIME(4) NOT NULL,
|
||||||
|
"margin" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "schedule_item_pkey" PRIMARY KEY ("uuid")
|
||||||
|
);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "ad_driver_idx" ON "ad"("driver");
|
CREATE INDEX "ad_driver_idx" ON "ad"("driver");
|
||||||
|
|
||||||
|
@ -66,3 +63,6 @@ CREATE INDEX "ad_fwdAzimuth_idx" ON "ad"("fwdAzimuth");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction");
|
CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "schedule_item" ADD CONSTRAINT "schedule_item_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -15,26 +15,15 @@ datasource db {
|
||||||
|
|
||||||
model Ad {
|
model Ad {
|
||||||
uuid String @id @db.Uuid
|
uuid String @id @db.Uuid
|
||||||
userUuid String @db.Uuid
|
|
||||||
driver Boolean
|
driver Boolean
|
||||||
passenger Boolean
|
passenger Boolean
|
||||||
frequency Frequency
|
frequency Frequency
|
||||||
fromDate DateTime @db.Date
|
fromDate DateTime @db.Date
|
||||||
toDate DateTime @db.Date
|
toDate DateTime @db.Date
|
||||||
monTime DateTime? @db.Timestamptz()
|
schedule ScheduleItem[]
|
||||||
tueTime DateTime? @db.Timestamptz()
|
seatsProposed Int @db.SmallInt
|
||||||
wedTime DateTime? @db.Timestamptz()
|
seatsRequested Int @db.SmallInt
|
||||||
thuTime DateTime? @db.Timestamptz()
|
strict Boolean
|
||||||
friTime DateTime? @db.Timestamptz()
|
|
||||||
satTime DateTime? @db.Timestamptz()
|
|
||||||
sunTime DateTime? @db.Timestamptz()
|
|
||||||
monMargin Int
|
|
||||||
tueMargin Int
|
|
||||||
wedMargin Int
|
|
||||||
thuMargin Int
|
|
||||||
friMargin Int
|
|
||||||
satMargin Int
|
|
||||||
sunMargin Int
|
|
||||||
driverDuration Int?
|
driverDuration Int?
|
||||||
driverDistance Int?
|
driverDistance Int?
|
||||||
passengerDuration Int?
|
passengerDuration Int?
|
||||||
|
@ -43,10 +32,6 @@ model Ad {
|
||||||
direction Unsupported("geography(LINESTRING)")?
|
direction Unsupported("geography(LINESTRING)")?
|
||||||
fwdAzimuth Int
|
fwdAzimuth Int
|
||||||
backAzimuth Int
|
backAzimuth Int
|
||||||
seatsDriver Int @db.SmallInt
|
|
||||||
seatsPassenger Int @db.SmallInt
|
|
||||||
seatsUsed Int @db.SmallInt
|
|
||||||
strict Boolean
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@ -59,6 +44,19 @@ model Ad {
|
||||||
@@map("ad")
|
@@map("ad")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ScheduleItem {
|
||||||
|
uuid String @id @default(uuid()) @db.Uuid
|
||||||
|
adUuid String @db.Uuid
|
||||||
|
day Int
|
||||||
|
time DateTime @db.Time(4)
|
||||||
|
margin Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("schedule_item")
|
||||||
|
}
|
||||||
|
|
||||||
enum Frequency {
|
enum Frequency {
|
||||||
PUNCTUAL
|
PUNCTUAL
|
||||||
RECURRENT
|
RECURRENT
|
||||||
|
|
|
@ -1,32 +1,68 @@
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
|
||||||
import { MatcherModule } from './modules/matcher/matcher.module';
|
|
||||||
import { AdModule } from './modules/ad/ad.module';
|
import { AdModule } from './modules/ad/ad.module';
|
||||||
import { ConfigurationModule } from '@mobicoop/configuration-module';
|
import {
|
||||||
|
ConfigurationModule,
|
||||||
|
ConfigurationModuleOptions,
|
||||||
|
} from '@mobicoop/configuration-module';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { RequestContextModule } from 'nestjs-request-context';
|
||||||
|
import { MessagerModule } from '@modules/messager/messager.module';
|
||||||
|
import { HealthModule, HealthRepositoryPort } from '@mobicoop/health-module';
|
||||||
|
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
|
||||||
|
import { HealthModuleOptions } from '@mobicoop/health-module/dist/core/domain/types/health.types';
|
||||||
|
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
import { GeographyModule } from '@modules/geography/geography.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
RequestContextModule,
|
||||||
ConfigurationModule.forRootAsync({
|
ConfigurationModule.forRootAsync({
|
||||||
setConfigurationBrokerRoutingKeys: [
|
imports: [ConfigModule],
|
||||||
'configuration.create',
|
inject: [ConfigService],
|
||||||
'configuration.update',
|
useFactory: async (
|
||||||
],
|
configService: ConfigService,
|
||||||
deleteConfigurationRoutingKey: 'configuration.delete',
|
): Promise<ConfigurationModuleOptions> => ({
|
||||||
propagateConfigurationRoutingKey: 'configuration.propagate',
|
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
|
||||||
|
messageBroker: {
|
||||||
|
uri: configService.get<string>('MESSAGE_BROKER_URI'),
|
||||||
|
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
host: configService.get<string>('REDIS_HOST'),
|
||||||
|
password: configService.get<string>('REDIS_PASSWORD'),
|
||||||
|
port: configService.get<number>('REDIS_PORT'),
|
||||||
|
},
|
||||||
setConfigurationBrokerQueue: 'matcher-configuration-create-update',
|
setConfigurationBrokerQueue: 'matcher-configuration-create-update',
|
||||||
deleteConfigurationQueue: 'matcher-configuration-delete',
|
deleteConfigurationQueue: 'matcher-configuration-delete',
|
||||||
propagateConfigurationQueue: 'matcher-configuration-propagate',
|
propagateConfigurationQueue: 'matcher-configuration-propagate',
|
||||||
}),
|
}),
|
||||||
AutomapperModule.forRoot({ strategyInitializer: classes() }),
|
}),
|
||||||
HealthModule,
|
HealthModule.forRootAsync({
|
||||||
MatcherModule,
|
imports: [AdModule, MessagerModule],
|
||||||
AdModule,
|
inject: [AD_REPOSITORY, MESSAGE_PUBLISHER],
|
||||||
|
useFactory: async (
|
||||||
|
adRepository: HealthRepositoryPort,
|
||||||
|
messagePublisher: MessagePublisherPort,
|
||||||
|
): Promise<HealthModuleOptions> => ({
|
||||||
|
serviceName: 'matcher',
|
||||||
|
criticalLoggingKey: 'logging.matcher.health.crit',
|
||||||
|
checkRepositories: [
|
||||||
|
{
|
||||||
|
name: 'AdRepository',
|
||||||
|
repository: adRepository,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
controllers: [],
|
messagePublisher,
|
||||||
providers: [],
|
}),
|
||||||
|
}),
|
||||||
|
AdModule,
|
||||||
|
GeographyModule,
|
||||||
|
MessagerModule,
|
||||||
|
],
|
||||||
|
exports: [AdModule, GeographyModule, MessagerModule],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
|
@ -2,7 +2,6 @@ syntax = "proto3";
|
||||||
|
|
||||||
package health;
|
package health;
|
||||||
|
|
||||||
|
|
||||||
service Health {
|
service Health {
|
||||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||||
}
|
}
|
||||||
|
@ -18,4 +17,5 @@ message HealthCheckResponse {
|
||||||
NOT_SERVING = 2;
|
NOT_SERVING = 2;
|
||||||
}
|
}
|
||||||
ServingStatus status = 1;
|
ServingStatus status = 1;
|
||||||
|
string message = 2;
|
||||||
}
|
}
|
|
@ -11,11 +11,8 @@ async function bootstrap() {
|
||||||
app.connectMicroservice<MicroserviceOptions>({
|
app.connectMicroservice<MicroserviceOptions>({
|
||||||
transport: Transport.GRPC,
|
transport: Transport.GRPC,
|
||||||
options: {
|
options: {
|
||||||
package: ['matcher', 'health'],
|
package: ['health'],
|
||||||
protoPath: [
|
protoPath: [join(__dirname, 'health.proto')],
|
||||||
join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'),
|
|
||||||
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
|
|
||||||
],
|
|
||||||
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,
|
||||||
loader: { keepCase: true },
|
loader: { keepCase: true },
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');
|
||||||
|
export const AD_DIRECTION_ENCODER = Symbol('AD_DIRECTION_ENCODER');
|
||||||
|
export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
|
||||||
|
export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol(
|
||||||
|
'AD_GET_BASIC_ROUTE_CONTROLLER',
|
||||||
|
);
|
||||||
|
export const AD_ROUTE_PROVIDER = Symbol('AD_ROUTE_PROVIDER');
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { AdEntity } from './core/domain/ad.entity';
|
||||||
|
import {
|
||||||
|
AdWriteModel,
|
||||||
|
AdReadModel,
|
||||||
|
ScheduleItemModel,
|
||||||
|
AdUnsupportedWriteModel,
|
||||||
|
} from './infrastructure/ad.repository';
|
||||||
|
import { Frequency } from './core/domain/ad.types';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
|
||||||
|
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||||
|
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
|
||||||
|
import { ExtendedMapper } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper constructs objects that are used in different layers:
|
||||||
|
* Record is an object that is stored in a database,
|
||||||
|
* Entity is an object that is used in application domain layer,
|
||||||
|
* and a ResponseDTO is an object returned to a user (usually as json).
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdMapper
|
||||||
|
implements
|
||||||
|
ExtendedMapper<
|
||||||
|
AdEntity,
|
||||||
|
AdReadModel,
|
||||||
|
AdWriteModel,
|
||||||
|
AdUnsupportedWriteModel,
|
||||||
|
undefined
|
||||||
|
>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_DIRECTION_ENCODER)
|
||||||
|
private readonly directionEncoder: DirectionEncoderPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
toPersistence = (entity: AdEntity): AdWriteModel => {
|
||||||
|
const copy = entity.getProps();
|
||||||
|
const now = new Date();
|
||||||
|
const record: AdWriteModel = {
|
||||||
|
uuid: copy.id,
|
||||||
|
driver: copy.driver,
|
||||||
|
passenger: copy.passenger,
|
||||||
|
frequency: copy.frequency,
|
||||||
|
fromDate: new Date(copy.fromDate),
|
||||||
|
toDate: new Date(copy.toDate),
|
||||||
|
schedule: {
|
||||||
|
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||||
|
uuid: v4(),
|
||||||
|
day: scheduleItem.day,
|
||||||
|
time: new Date(
|
||||||
|
1970,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
parseInt(scheduleItem.time.split(':')[0]),
|
||||||
|
parseInt(scheduleItem.time.split(':')[1]),
|
||||||
|
),
|
||||||
|
margin: scheduleItem.margin,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
seatsProposed: copy.seatsProposed,
|
||||||
|
seatsRequested: copy.seatsRequested,
|
||||||
|
strict: copy.strict,
|
||||||
|
driverDuration: copy.driverDuration,
|
||||||
|
driverDistance: copy.driverDistance,
|
||||||
|
passengerDuration: copy.passengerDuration,
|
||||||
|
passengerDistance: copy.passengerDistance,
|
||||||
|
fwdAzimuth: copy.fwdAzimuth,
|
||||||
|
backAzimuth: copy.backAzimuth,
|
||||||
|
createdAt: copy.createdAt,
|
||||||
|
updatedAt: copy.updatedAt,
|
||||||
|
};
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
toDomain = (record: AdReadModel): AdEntity => {
|
||||||
|
const entity = new AdEntity({
|
||||||
|
id: record.uuid,
|
||||||
|
createdAt: new Date(record.createdAt),
|
||||||
|
updatedAt: new Date(record.updatedAt),
|
||||||
|
props: {
|
||||||
|
driver: record.driver,
|
||||||
|
passenger: record.passenger,
|
||||||
|
frequency: Frequency[record.frequency],
|
||||||
|
fromDate: record.fromDate.toISOString().split('T')[0],
|
||||||
|
toDate: record.toDate.toISOString().split('T')[0],
|
||||||
|
schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({
|
||||||
|
day: scheduleItem.day,
|
||||||
|
time: `${scheduleItem.time
|
||||||
|
.getUTCHours()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}:${scheduleItem.time
|
||||||
|
.getUTCMinutes()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`,
|
||||||
|
margin: scheduleItem.margin,
|
||||||
|
})),
|
||||||
|
seatsProposed: record.seatsProposed,
|
||||||
|
seatsRequested: record.seatsRequested,
|
||||||
|
strict: record.strict,
|
||||||
|
driverDuration: record.driverDuration,
|
||||||
|
driverDistance: record.driverDistance,
|
||||||
|
passengerDuration: record.passengerDuration,
|
||||||
|
passengerDistance: record.passengerDistance,
|
||||||
|
waypoints: this.directionEncoder
|
||||||
|
.decode(record.waypoints)
|
||||||
|
.map((coordinates, index) => ({
|
||||||
|
position: index,
|
||||||
|
...coordinates,
|
||||||
|
})),
|
||||||
|
fwdAzimuth: record.fwdAzimuth,
|
||||||
|
backAzimuth: record.backAzimuth,
|
||||||
|
points: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return entity;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
toResponse = (entity: AdEntity): undefined => {
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
toUnsupportedPersistence = (entity: AdEntity): AdUnsupportedWriteModel => ({
|
||||||
|
waypoints: this.directionEncoder.encode(entity.getProps().waypoints),
|
||||||
|
direction: this.directionEncoder.encode(entity.getProps().points),
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,72 +1,71 @@
|
||||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
||||||
import { AdMessagerController } from './adapters/primaries/ad-messager.controller';
|
|
||||||
import { AdProfile } from './mappers/ad.profile';
|
|
||||||
import { CreateAdUseCase } from './domain/usecases/create-ad.usecase';
|
|
||||||
import { AdRepository } from './adapters/secondaries/ad.repository';
|
|
||||||
import { DatabaseModule } from '../database/database.module';
|
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
import { Messager } from './adapters/secondaries/messager';
|
import {
|
||||||
import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder';
|
AD_MESSAGE_PUBLISHER,
|
||||||
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
|
AD_REPOSITORY,
|
||||||
import { GeorouterCreator } from '../geography/adapters/secondaries/georouter-creator';
|
AD_DIRECTION_ENCODER,
|
||||||
import { GeographyModule } from '../geography/geography.module';
|
AD_ROUTE_PROVIDER,
|
||||||
import { HttpModule } from '@nestjs/axios';
|
AD_GET_BASIC_ROUTE_CONTROLLER,
|
||||||
import { PostgresDirectionEncoder } from '../geography/adapters/secondaries/postgres-direction-encoder';
|
} from './ad.di-tokens';
|
||||||
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
|
import { AdRepository } from './infrastructure/ad.repository';
|
||||||
|
import { PrismaService } from './infrastructure/prisma.service';
|
||||||
|
import { AdMapper } from './ad.mapper';
|
||||||
|
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||||
|
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||||
|
import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller';
|
||||||
|
import { RouteProvider } from './infrastructure/route-provider';
|
||||||
|
import { GeographyModule } from '@modules/geography/geography.module';
|
||||||
|
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||||
|
|
||||||
@Module({
|
const messageHandlers = [AdCreatedMessageHandler];
|
||||||
imports: [
|
|
||||||
GeographyModule,
|
const commandHandlers: Provider[] = [CreateAdService];
|
||||||
DatabaseModule,
|
|
||||||
CqrsModule,
|
const mappers: Provider[] = [AdMapper];
|
||||||
HttpModule,
|
|
||||||
RabbitMQModule.forRootAsync(RabbitMQModule, {
|
const repositories: Provider[] = [
|
||||||
imports: [ConfigModule],
|
|
||||||
useFactory: async (configService: ConfigService) => ({
|
|
||||||
exchanges: [
|
|
||||||
{
|
{
|
||||||
name: configService.get<string>('RMQ_EXCHANGE'),
|
provide: AD_REPOSITORY,
|
||||||
type: 'topic',
|
useClass: AdRepository,
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
handlers: {
|
|
||||||
adCreated: {
|
const messagePublishers: Provider[] = [
|
||||||
exchange: configService.get<string>('RMQ_EXCHANGE'),
|
|
||||||
routingKey: 'ad.created',
|
|
||||||
queue: 'matcher-ad-created',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uri: configService.get<string>('RMQ_URI'),
|
|
||||||
connectionInitOptions: { wait: false },
|
|
||||||
enableControllerDiscovery: true,
|
|
||||||
}),
|
|
||||||
inject: [ConfigService],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [AdMessagerController],
|
|
||||||
providers: [
|
|
||||||
{
|
{
|
||||||
provide: 'ParamsProvider',
|
provide: AD_MESSAGE_PUBLISHER,
|
||||||
useClass: DefaultParamsProvider,
|
useExisting: MessageBrokerPublisher,
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const orms: Provider[] = [PrismaService];
|
||||||
|
|
||||||
|
const adapters: Provider[] = [
|
||||||
{
|
{
|
||||||
provide: 'GeorouterCreator',
|
provide: AD_DIRECTION_ENCODER,
|
||||||
useClass: GeorouterCreator,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'TimezoneFinder',
|
|
||||||
useClass: GeoTimezoneFinder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'DirectionEncoder',
|
|
||||||
useClass: PostgresDirectionEncoder,
|
useClass: PostgresDirectionEncoder,
|
||||||
},
|
},
|
||||||
AdProfile,
|
{
|
||||||
Messager,
|
provide: AD_ROUTE_PROVIDER,
|
||||||
AdRepository,
|
useClass: RouteProvider,
|
||||||
CreateAdUseCase,
|
},
|
||||||
|
{
|
||||||
|
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
|
||||||
|
useClass: GetBasicRouteController,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [CqrsModule, GeographyModule],
|
||||||
|
providers: [
|
||||||
|
...messageHandlers,
|
||||||
|
...commandHandlers,
|
||||||
|
...mappers,
|
||||||
|
...repositories,
|
||||||
|
...messagePublishers,
|
||||||
|
...orms,
|
||||||
|
...adapters,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [PrismaService, AdMapper, AD_REPOSITORY, AD_DIRECTION_ENCODER],
|
||||||
})
|
})
|
||||||
export class AdModule {}
|
export class AdModule {}
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { Controller } from '@nestjs/common';
|
|
||||||
import { CommandBus, QueryBus } from '@nestjs/cqrs';
|
|
||||||
import { CreateAdCommand } from '../../commands/create-ad.command';
|
|
||||||
import { CreateAdRequest } from '../../domain/dtos/create-ad.request';
|
|
||||||
import { validateOrReject } from 'class-validator';
|
|
||||||
import { Messager } from '../secondaries/messager';
|
|
||||||
import { plainToInstance } from 'class-transformer';
|
|
||||||
import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
|
|
||||||
import { ExceptionCode } from 'src/modules/utils/exception-code.enum';
|
|
||||||
|
|
||||||
@Controller()
|
|
||||||
export class AdMessagerController {
|
|
||||||
constructor(
|
|
||||||
private readonly messager: Messager,
|
|
||||||
private readonly commandBus: CommandBus,
|
|
||||||
private readonly queryBus: QueryBus,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@RabbitSubscribe({
|
|
||||||
name: 'adCreated',
|
|
||||||
})
|
|
||||||
async adCreatedHandler(message: string): Promise<void> {
|
|
||||||
let createAdRequest: CreateAdRequest;
|
|
||||||
// parse message to request instance
|
|
||||||
try {
|
|
||||||
createAdRequest = plainToInstance(CreateAdRequest, JSON.parse(message));
|
|
||||||
// validate instance
|
|
||||||
await validateOrReject(createAdRequest);
|
|
||||||
// validate nested objects (fixes direct nested validation bug)
|
|
||||||
for (const waypoint of createAdRequest.waypoints) {
|
|
||||||
try {
|
|
||||||
await validateOrReject(waypoint);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.messager.publish(
|
|
||||||
'matcher.ad.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
message: `Can't validate message : ${message}`,
|
|
||||||
error: e,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.commandBus.execute(new CreateAdCommand(createAdRequest));
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof DatabaseException) {
|
|
||||||
if (e.message.includes('already exists')) {
|
|
||||||
this.messager.publish(
|
|
||||||
'matcher.ad.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
code: ExceptionCode.ALREADY_EXISTS,
|
|
||||||
message: 'Already exists',
|
|
||||||
uuid: createAdRequest.uuid,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (e.message.includes("Can't reach database server")) {
|
|
||||||
this.messager.publish(
|
|
||||||
'matcher.ad.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
code: ExceptionCode.UNAVAILABLE,
|
|
||||||
message: 'Database server unavailable',
|
|
||||||
uuid: createAdRequest.uuid,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.messager.publish(
|
|
||||||
'logging.matcher.ad.crit',
|
|
||||||
JSON.stringify({
|
|
||||||
message,
|
|
||||||
error: e,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { DatabaseRepository } from '../../../database/domain/database.repository';
|
|
||||||
import { Ad } from '../../domain/entities/ad';
|
|
||||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AdRepository extends DatabaseRepository<Ad> {
|
|
||||||
protected model = 'ad';
|
|
||||||
|
|
||||||
createAd = async (ad: Partial<Ad>): Promise<Ad> => {
|
|
||||||
try {
|
|
||||||
const affectedRowNumber = await this.createWithFields(
|
|
||||||
this.createFields(ad),
|
|
||||||
);
|
|
||||||
if (affectedRowNumber == 1) {
|
|
||||||
return this.findOneByUuid(ad.uuid);
|
|
||||||
}
|
|
||||||
throw new DatabaseException();
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private createFields = (ad: Partial<Ad>): Partial<AdFields> => ({
|
|
||||||
uuid: `'${ad.uuid}'`,
|
|
||||||
userUuid: `'${ad.userUuid}'`,
|
|
||||||
driver: ad.driver ? 'true' : 'false',
|
|
||||||
passenger: ad.passenger ? 'true' : 'false',
|
|
||||||
frequency: `'${ad.frequency}'`,
|
|
||||||
fromDate: `'${ad.fromDate.getFullYear()}-${
|
|
||||||
ad.fromDate.getMonth() + 1
|
|
||||||
}-${ad.fromDate.getDate()}'`,
|
|
||||||
toDate: `'${ad.toDate.getFullYear()}-${
|
|
||||||
ad.toDate.getMonth() + 1
|
|
||||||
}-${ad.toDate.getDate()}'`,
|
|
||||||
monTime: ad.monTime
|
|
||||||
? `'${ad.monTime.getFullYear()}-${
|
|
||||||
ad.monTime.getMonth() + 1
|
|
||||||
}-${ad.monTime.getDate()}T${ad.monTime.getHours()}:${ad.monTime.getMinutes()}Z'`
|
|
||||||
: 'NULL',
|
|
||||||
tueTime: ad.tueTime
|
|
||||||
? `'${ad.tueTime.getFullYear()}-${
|
|
||||||
ad.tueTime.getMonth() + 1
|
|
||||||
}-${ad.tueTime.getDate()}T${ad.tueTime.getHours()}:${ad.tueTime.getMinutes()}Z'`
|
|
||||||
: 'NULL',
|
|
||||||
wedTime: ad.wedTime
|
|
||||||
? `'${ad.wedTime.getFullYear()}-${
|
|
||||||
ad.wedTime.getMonth() + 1
|
|
||||||
}-${ad.wedTime.getDate()}T${ad.wedTime.getHours()}:${ad.wedTime.getMinutes()}Z'`
|
|
||||||
: 'NULL',
|
|
||||||
thuTime: ad.thuTime
|
|
||||||
? `'${ad.thuTime.getFullYear()}-${
|
|
||||||
ad.thuTime.getMonth() + 1
|
|
||||||
}-${ad.thuTime.getDate()}T${ad.thuTime.getHours()}:${ad.thuTime.getMinutes()}Z'`
|
|
||||||
: 'NULL',
|
|
||||||
friTime: ad.friTime
|
|
||||||
? `'${ad.friTime.getFullYear()}-${
|
|
||||||
ad.friTime.getMonth() + 1
|
|
||||||
}-${ad.friTime.getDate()}T${ad.friTime.getHours()}:${ad.friTime.getMinutes()}Z'`
|
|
||||||
: 'NULL',
|
|
||||||
satTime: ad.satTime
|
|
||||||
? `'${ad.satTime.getFullYear()}-${
|
|
||||||
ad.satTime.getMonth() + 1
|
|
||||||
}-${ad.satTime.getDate()}T${ad.satTime.getHours()}:${ad.satTime.getMinutes()}Z'`
|
|
||||||
: 'NULL',
|
|
||||||
sunTime: ad.sunTime
|
|
||||||
? `'${ad.sunTime.getFullYear()}-${
|
|
||||||
ad.sunTime.getMonth() + 1
|
|
||||||
}-${ad.sunTime.getDate()}T${ad.sunTime.getHours()}:${ad.sunTime.getMinutes()}Z'`
|
|
||||||
: 'NULL',
|
|
||||||
monMargin: ad.monMargin,
|
|
||||||
tueMargin: ad.tueMargin,
|
|
||||||
wedMargin: ad.wedMargin,
|
|
||||||
thuMargin: ad.thuMargin,
|
|
||||||
friMargin: ad.friMargin,
|
|
||||||
satMargin: ad.satMargin,
|
|
||||||
sunMargin: ad.sunMargin,
|
|
||||||
fwdAzimuth: ad.fwdAzimuth,
|
|
||||||
backAzimuth: ad.backAzimuth,
|
|
||||||
driverDuration: ad.driverDuration ?? 'NULL',
|
|
||||||
driverDistance: ad.driverDistance ?? 'NULL',
|
|
||||||
passengerDuration: ad.passengerDuration ?? 'NULL',
|
|
||||||
passengerDistance: ad.passengerDistance ?? 'NULL',
|
|
||||||
waypoints: ad.waypoints,
|
|
||||||
direction: ad.direction,
|
|
||||||
seatsDriver: ad.seatsDriver,
|
|
||||||
seatsPassenger: ad.seatsPassenger,
|
|
||||||
seatsUsed: ad.seatsUsed ?? 0,
|
|
||||||
strict: ad.strict,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdFields = {
|
|
||||||
uuid: string;
|
|
||||||
userUuid: string;
|
|
||||||
driver: string;
|
|
||||||
passenger: string;
|
|
||||||
frequency: string;
|
|
||||||
fromDate: string;
|
|
||||||
toDate: string;
|
|
||||||
monTime: string;
|
|
||||||
tueTime: string;
|
|
||||||
wedTime: string;
|
|
||||||
thuTime: string;
|
|
||||||
friTime: string;
|
|
||||||
satTime: string;
|
|
||||||
sunTime: string;
|
|
||||||
monMargin: number;
|
|
||||||
tueMargin: number;
|
|
||||||
wedMargin: number;
|
|
||||||
thuMargin: number;
|
|
||||||
friMargin: number;
|
|
||||||
satMargin: number;
|
|
||||||
sunMargin: number;
|
|
||||||
driverDuration?: number | 'NULL';
|
|
||||||
driverDistance?: number | 'NULL';
|
|
||||||
passengerDuration?: number | 'NULL';
|
|
||||||
passengerDistance?: number | 'NULL';
|
|
||||||
waypoints: string;
|
|
||||||
direction: string;
|
|
||||||
fwdAzimuth: number;
|
|
||||||
backAzimuth: number;
|
|
||||||
seatsDriver?: number;
|
|
||||||
seatsPassenger?: number;
|
|
||||||
seatsUsed?: number;
|
|
||||||
strict: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { DefaultParams } from '../../domain/types/default-params.type';
|
|
||||||
import { IProvideParams } from '../../domain/interfaces/params-provider.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DefaultParamsProvider implements IProvideParams {
|
|
||||||
constructor(private readonly configService: ConfigService) {}
|
|
||||||
|
|
||||||
getParams = (): DefaultParams => {
|
|
||||||
return {
|
|
||||||
DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
|
|
||||||
GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'),
|
|
||||||
GEOROUTER_URL: this.configService.get('GEOROUTER_URL'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export abstract class MessageBroker {
|
|
||||||
exchange: string;
|
|
||||||
|
|
||||||
constructor(exchange: string) {
|
|
||||||
this.exchange = exchange;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract publish(routingKey: string, message: string): void;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { MessageBroker } from './message-broker';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Messager extends MessageBroker {
|
|
||||||
constructor(
|
|
||||||
private readonly amqpConnection: AmqpConnection,
|
|
||||||
configService: ConfigService,
|
|
||||||
) {
|
|
||||||
super(configService.get<string>('RMQ_EXCHANGE'));
|
|
||||||
}
|
|
||||||
|
|
||||||
publish = (routingKey: string, message: string): void => {
|
|
||||||
this.amqpConnection.publish(this.exchange, routingKey, message);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder';
|
|
||||||
import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TimezoneFinder implements IFindTimezone {
|
|
||||||
constructor(private readonly geoTimezoneFinder: GeoTimezoneFinder) {}
|
|
||||||
|
|
||||||
timezones = (lon: number, lat: number): string[] =>
|
|
||||||
this.geoTimezoneFinder.timezones(lon, lat);
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { CreateAdRequest } from '../domain/dtos/create-ad.request';
|
|
||||||
|
|
||||||
export class CreateAdCommand {
|
|
||||||
readonly createAdRequest: CreateAdRequest;
|
|
||||||
|
|
||||||
constructor(request: CreateAdRequest) {
|
|
||||||
this.createAdRequest = request;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
import { ScheduleItem } from '../../types/schedule-item.type';
|
||||||
|
import { Waypoint } from '../../types/waypoint.type';
|
||||||
|
|
||||||
|
export class CreateAdCommand extends Command {
|
||||||
|
readonly id: string;
|
||||||
|
readonly driver: boolean;
|
||||||
|
readonly passenger: boolean;
|
||||||
|
readonly frequency: Frequency;
|
||||||
|
readonly fromDate: string;
|
||||||
|
readonly toDate: string;
|
||||||
|
readonly schedule: ScheduleItem[];
|
||||||
|
readonly seatsProposed: number;
|
||||||
|
readonly seatsRequested: number;
|
||||||
|
readonly strict: boolean;
|
||||||
|
readonly waypoints: Waypoint[];
|
||||||
|
|
||||||
|
constructor(props: CommandProps<CreateAdCommand>) {
|
||||||
|
super(props);
|
||||||
|
this.id = props.id;
|
||||||
|
this.driver = props.driver;
|
||||||
|
this.passenger = props.passenger;
|
||||||
|
this.frequency = props.frequency;
|
||||||
|
this.fromDate = props.fromDate;
|
||||||
|
this.toDate = props.toDate;
|
||||||
|
this.schedule = props.schedule;
|
||||||
|
this.seatsProposed = props.seatsProposed;
|
||||||
|
this.seatsRequested = props.seatsRequested;
|
||||||
|
this.strict = props.strict;
|
||||||
|
this.waypoints = props.waypoints;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { CreateAdCommand } from './create-ad.command';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
|
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||||
|
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||||
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
|
import { RouteProviderPort } from '../../ports/route-provider.port';
|
||||||
|
import { Role } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { Route } from '../../types/route.type';
|
||||||
|
|
||||||
|
@CommandHandler(CreateAdCommand)
|
||||||
|
export class CreateAdService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_REPOSITORY)
|
||||||
|
private readonly repository: AdRepositoryPort,
|
||||||
|
@Inject(AD_ROUTE_PROVIDER)
|
||||||
|
private readonly routeProvider: RouteProviderPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: CreateAdCommand): Promise<AggregateID> {
|
||||||
|
const roles: Role[] = [];
|
||||||
|
if (command.driver) roles.push(Role.DRIVER);
|
||||||
|
if (command.passenger) roles.push(Role.PASSENGER);
|
||||||
|
const route: Route = await this.routeProvider.getBasic(
|
||||||
|
roles,
|
||||||
|
command.waypoints,
|
||||||
|
);
|
||||||
|
const ad = AdEntity.create({
|
||||||
|
id: command.id,
|
||||||
|
driver: command.driver,
|
||||||
|
passenger: command.passenger,
|
||||||
|
frequency: command.frequency,
|
||||||
|
fromDate: command.fromDate,
|
||||||
|
toDate: command.toDate,
|
||||||
|
schedule: command.schedule,
|
||||||
|
seatsProposed: command.seatsProposed,
|
||||||
|
seatsRequested: command.seatsRequested,
|
||||||
|
strict: command.strict,
|
||||||
|
waypoints: command.waypoints,
|
||||||
|
points: route.points,
|
||||||
|
driverDistance: route.driverDistance,
|
||||||
|
driverDuration: route.driverDuration,
|
||||||
|
passengerDistance: route.passengerDistance,
|
||||||
|
passengerDuration: route.passengerDuration,
|
||||||
|
fwdAzimuth: route.fwdAzimuth,
|
||||||
|
backAzimuth: route.backAzimuth,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.repository.insertWithUnsupportedFields(ad, 'ad');
|
||||||
|
return ad.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new AdAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { ExtendedRepositoryPort } from '@mobicoop/ddd-library';
|
||||||
|
import { AdEntity } from '../../domain/ad.entity';
|
||||||
|
|
||||||
|
export type AdRepositoryPort = ExtendedRepositoryPort<AdEntity>;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Role } from '../../domain/ad.types';
|
||||||
|
import { Waypoint } from '../types/waypoint.type';
|
||||||
|
import { Route } from '../types/route.type';
|
||||||
|
|
||||||
|
export interface RouteProviderPort {
|
||||||
|
/**
|
||||||
|
* Get a basic route with points and overall duration / distance
|
||||||
|
*/
|
||||||
|
getBasic(roles: Role[], waypoints: Waypoint[]): Promise<Route>;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type Coordinates = {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Coordinates } from './coordinates.type';
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
driverDistance?: number;
|
||||||
|
driverDuration?: number;
|
||||||
|
passengerDistance?: number;
|
||||||
|
passengerDuration?: number;
|
||||||
|
fwdAzimuth: number;
|
||||||
|
backAzimuth: number;
|
||||||
|
points: Coordinates[];
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type ScheduleItem = {
|
||||||
|
day: number;
|
||||||
|
time: string;
|
||||||
|
margin: number;
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Coordinates } from './coordinates.type';
|
||||||
|
|
||||||
|
export type Waypoint = {
|
||||||
|
position: number;
|
||||||
|
} & Coordinates;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import { AdProps, CreateAdProps } from './ad.types';
|
||||||
|
|
||||||
|
export class AdEntity extends AggregateRoot<AdProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = (create: CreateAdProps): AdEntity => {
|
||||||
|
const props: AdProps = { ...create };
|
||||||
|
return new AdEntity({ id: create.id, props });
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class AdAlreadyExistsException extends ExceptionBase {
|
||||||
|
static readonly message = 'Ad already exists';
|
||||||
|
|
||||||
|
public readonly code = 'AD.ALREADY_EXISTS';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(AdAlreadyExistsException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { PointProps } from './value-objects/point.value-object';
|
||||||
|
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 {
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: Frequency;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
schedule: ScheduleItemProps[];
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
driverDuration?: number;
|
||||||
|
driverDistance?: number;
|
||||||
|
passengerDuration?: number;
|
||||||
|
passengerDistance?: number;
|
||||||
|
waypoints: WaypointProps[];
|
||||||
|
points: PointProps[];
|
||||||
|
fwdAzimuth: number;
|
||||||
|
backAzimuth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties that are needed for an Ad creation
|
||||||
|
export interface CreateAdProps {
|
||||||
|
id: string;
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: Frequency;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
schedule: ScheduleItemProps[];
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
waypoints: WaypointProps[];
|
||||||
|
driverDuration?: number;
|
||||||
|
driverDistance?: number;
|
||||||
|
passengerDuration?: number;
|
||||||
|
passengerDistance?: number;
|
||||||
|
fwdAzimuth: number;
|
||||||
|
backAzimuth: number;
|
||||||
|
points: PointProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Frequency {
|
||||||
|
PUNCTUAL = 'PUNCTUAL',
|
||||||
|
RECURRENT = 'RECURRENT',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Role {
|
||||||
|
DRIVER = 'DRIVER',
|
||||||
|
PASSENGER = 'PASSENGER',
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
ValueObject,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface PointProps {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Point extends ValueObject<PointProps> {
|
||||||
|
get lon(): number {
|
||||||
|
return this.props.lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat(): number {
|
||||||
|
return this.props.lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate(props: PointProps): void {
|
||||||
|
if (props.lon > 180 || props.lon < -180)
|
||||||
|
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
|
||||||
|
if (props.lat > 90 || props.lat < -90)
|
||||||
|
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import {
|
||||||
|
ArgumentInvalidException,
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
ValueObject,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface ScheduleItemProps {
|
||||||
|
day: number;
|
||||||
|
time: string;
|
||||||
|
margin: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
|
||||||
|
get day(): number | undefined {
|
||||||
|
return this.props.day;
|
||||||
|
}
|
||||||
|
|
||||||
|
get time(): string {
|
||||||
|
return this.props.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
get margin(): number | undefined {
|
||||||
|
return this.props.margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: ScheduleItemProps): void {
|
||||||
|
if (props.day < 0 || props.day > 6)
|
||||||
|
throw new ArgumentOutOfRangeException('day must be between 0 and 6');
|
||||||
|
if (props.time.split(':').length != 2)
|
||||||
|
throw new ArgumentInvalidException('time is invalid');
|
||||||
|
if (
|
||||||
|
parseInt(props.time.split(':')[0]) < 0 ||
|
||||||
|
parseInt(props.time.split(':')[0]) > 23
|
||||||
|
)
|
||||||
|
throw new ArgumentInvalidException('time is invalid');
|
||||||
|
if (
|
||||||
|
parseInt(props.time.split(':')[1]) < 0 ||
|
||||||
|
parseInt(props.time.split(':')[1]) > 59
|
||||||
|
)
|
||||||
|
throw new ArgumentInvalidException('time is invalid');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import {
|
||||||
|
ArgumentInvalidException,
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
ValueObject,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface WaypointProps {
|
||||||
|
position: number;
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Waypoint extends ValueObject<WaypointProps> {
|
||||||
|
get position(): number {
|
||||||
|
return this.props.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lon(): number {
|
||||||
|
return this.props.lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat(): number {
|
||||||
|
return this.props.lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate(props: WaypointProps): void {
|
||||||
|
if (props.position < 0)
|
||||||
|
throw new ArgumentInvalidException(
|
||||||
|
'position must be greater than or equal to 0',
|
||||||
|
);
|
||||||
|
if (props.lon > 180 || props.lon < -180)
|
||||||
|
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
|
||||||
|
if (props.lat > 90 || props.lat < -90)
|
||||||
|
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,140 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import {
|
|
||||||
ArrayMinSize,
|
|
||||||
IsArray,
|
|
||||||
IsBoolean,
|
|
||||||
IsDate,
|
|
||||||
IsEnum,
|
|
||||||
IsMilitaryTime,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsNumber,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { Frequency } from '../types/frequency.enum';
|
|
||||||
import { Coordinate } from '../../../geography/domain/entities/coordinate';
|
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
import { HasTruthyWith } from './has-truthy-with.validator';
|
|
||||||
|
|
||||||
export class CreateAdRequest {
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@AutoMap()
|
|
||||||
userUuid: string;
|
|
||||||
|
|
||||||
@HasTruthyWith('passenger', {
|
|
||||||
message: 'A role (driver or passenger) must be set to true',
|
|
||||||
})
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
driver: boolean;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
passenger: boolean;
|
|
||||||
|
|
||||||
@IsEnum(Frequency)
|
|
||||||
@AutoMap()
|
|
||||||
frequency: Frequency;
|
|
||||||
|
|
||||||
@Type(() => Date)
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
fromDate: Date;
|
|
||||||
|
|
||||||
@Type(() => Date)
|
|
||||||
@IsDate()
|
|
||||||
@AutoMap()
|
|
||||||
toDate: Date;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsMilitaryTime()
|
|
||||||
@AutoMap()
|
|
||||||
monTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsMilitaryTime()
|
|
||||||
@AutoMap()
|
|
||||||
tueTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsMilitaryTime()
|
|
||||||
@AutoMap()
|
|
||||||
wedTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsMilitaryTime()
|
|
||||||
@AutoMap()
|
|
||||||
thuTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsMilitaryTime()
|
|
||||||
@AutoMap()
|
|
||||||
friTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsMilitaryTime()
|
|
||||||
@AutoMap()
|
|
||||||
satTime?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsMilitaryTime()
|
|
||||||
@AutoMap()
|
|
||||||
sunTime?: string;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
monMargin: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
tueMargin: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
wedMargin: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
thuMargin: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
friMargin: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
satMargin: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
sunMargin: number;
|
|
||||||
|
|
||||||
@Type(() => Coordinate)
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMinSize(2)
|
|
||||||
@AutoMap(() => [Coordinate])
|
|
||||||
waypoints: Coordinate[];
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
seatsDriver: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
seatsPassenger: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@AutoMap()
|
|
||||||
seatsUsed?: number;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@AutoMap()
|
|
||||||
strict: boolean;
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import {
|
|
||||||
registerDecorator,
|
|
||||||
ValidationOptions,
|
|
||||||
ValidationArguments,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
export function HasTruthyWith(
|
|
||||||
property: string,
|
|
||||||
validationOptions?: ValidationOptions,
|
|
||||||
) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
return function (object: Object, propertyName: string) {
|
|
||||||
registerDecorator({
|
|
||||||
name: 'hasTruthyWith',
|
|
||||||
target: object.constructor,
|
|
||||||
propertyName: propertyName,
|
|
||||||
constraints: [property],
|
|
||||||
options: validationOptions,
|
|
||||||
validator: {
|
|
||||||
validate(value: any, args: ValidationArguments) {
|
|
||||||
const [relatedPropertyName] = args.constraints;
|
|
||||||
const relatedValue = (args.object as any)[relatedPropertyName];
|
|
||||||
return (
|
|
||||||
typeof value === 'boolean' &&
|
|
||||||
typeof relatedValue === 'boolean' &&
|
|
||||||
(value || relatedValue)
|
|
||||||
); // you can return a Promise<boolean> here as well, if you want to make async validation
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { Frequency } from '../types/frequency.enum';
|
|
||||||
|
|
||||||
export class Ad {
|
|
||||||
@AutoMap()
|
|
||||||
uuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
userUuid: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
driver: boolean;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
passenger: boolean;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
frequency: Frequency;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
fromDate: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
toDate: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
monTime: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
tueTime: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
wedTime: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
thuTime: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
friTime: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
satTime: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
sunTime: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
monMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
tueMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
wedMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
thuMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
friMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
satMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
sunMargin: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
driverDuration?: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
driverDistance?: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
passengerDuration?: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
passengerDistance?: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
waypoints: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
direction: string;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
fwdAzimuth: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
backAzimuth: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
seatsDriver: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
seatsPassenger: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
seatsUsed: number;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
strict: boolean;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@AutoMap()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
import { Coordinate } from '../../../geography/domain/entities/coordinate';
|
|
||||||
import { Route } from '../../../geography/domain/entities/route';
|
|
||||||
import { Role } from '../types/role.enum';
|
|
||||||
import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface';
|
|
||||||
import { Path } from '../../../geography/domain/types/path.type';
|
|
||||||
import { GeorouterSettings } from '../../../geography/domain/types/georouter-settings.type';
|
|
||||||
|
|
||||||
export class Geography {
|
|
||||||
private coordinates: Coordinate[];
|
|
||||||
driverRoute: Route;
|
|
||||||
passengerRoute: Route;
|
|
||||||
|
|
||||||
constructor(coordinates: Coordinate[]) {
|
|
||||||
this.coordinates = coordinates;
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoutes = async (
|
|
||||||
roles: Role[],
|
|
||||||
georouter: IGeorouter,
|
|
||||||
settings: GeorouterSettings,
|
|
||||||
): Promise<void> => {
|
|
||||||
const paths: Path[] = this.getPaths(roles);
|
|
||||||
const routes = await georouter.route(paths, settings);
|
|
||||||
if (routes.some((route) => route.key == RouteKey.COMMON)) {
|
|
||||||
this.driverRoute = routes.find(
|
|
||||||
(route) => route.key == RouteKey.COMMON,
|
|
||||||
).route;
|
|
||||||
this.passengerRoute = routes.find(
|
|
||||||
(route) => route.key == RouteKey.COMMON,
|
|
||||||
).route;
|
|
||||||
} else {
|
|
||||||
if (routes.some((route) => route.key == RouteKey.DRIVER)) {
|
|
||||||
this.driverRoute = routes.find(
|
|
||||||
(route) => route.key == RouteKey.DRIVER,
|
|
||||||
).route;
|
|
||||||
}
|
|
||||||
if (routes.some((route) => route.key == RouteKey.PASSENGER)) {
|
|
||||||
this.passengerRoute = routes.find(
|
|
||||||
(route) => route.key == RouteKey.PASSENGER,
|
|
||||||
).route;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private getPaths = (roles: Role[]): Path[] => {
|
|
||||||
const paths: Path[] = [];
|
|
||||||
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
|
|
||||||
if (this.coordinates.length == 2) {
|
|
||||||
// 2 points => same route for driver and passenger
|
|
||||||
const commonPath: Path = {
|
|
||||||
key: RouteKey.COMMON,
|
|
||||||
points: this.coordinates,
|
|
||||||
};
|
|
||||||
paths.push(commonPath);
|
|
||||||
} else {
|
|
||||||
const driverPath: Path = this.createDriverPath();
|
|
||||||
const passengerPath: Path = this.createPassengerPath();
|
|
||||||
paths.push(driverPath, passengerPath);
|
|
||||||
}
|
|
||||||
} else if (roles.includes(Role.DRIVER)) {
|
|
||||||
const driverPath: Path = this.createDriverPath();
|
|
||||||
paths.push(driverPath);
|
|
||||||
} else if (roles.includes(Role.PASSENGER)) {
|
|
||||||
const passengerPath: Path = this.createPassengerPath();
|
|
||||||
paths.push(passengerPath);
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
};
|
|
||||||
|
|
||||||
private createDriverPath = (): Path => {
|
|
||||||
return {
|
|
||||||
key: RouteKey.DRIVER,
|
|
||||||
points: this.coordinates,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
private createPassengerPath = (): Path => {
|
|
||||||
return {
|
|
||||||
key: RouteKey.PASSENGER,
|
|
||||||
points: [
|
|
||||||
this.coordinates[0],
|
|
||||||
this.coordinates[this.coordinates.length - 1],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RouteKey {
|
|
||||||
COMMON = 'common',
|
|
||||||
DRIVER = 'driver',
|
|
||||||
PASSENGER = 'passenger',
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { DateTime, TimeZone } from 'timezonecomplete';
|
|
||||||
|
|
||||||
export class TimeConverter {
|
|
||||||
static toUtcDatetime = (date: Date, time: string, timezone: string): Date => {
|
|
||||||
try {
|
|
||||||
if (!date || !time || !timezone) throw new Error();
|
|
||||||
return new Date(
|
|
||||||
new DateTime(
|
|
||||||
`${date.toISOString().split('T')[0]}T${time}:00`,
|
|
||||||
TimeZone.zone(timezone, false),
|
|
||||||
)
|
|
||||||
.convert(TimeZone.zone('UTC'))
|
|
||||||
.toIsoString(),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum Frequency {
|
|
||||||
PUNCTUAL = 'PUNCTUAL',
|
|
||||||
RECURRENT = 'RECURRENT',
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export enum Role {
|
|
||||||
DRIVER = 'DRIVER',
|
|
||||||
PASSENGER = 'PASSENGER',
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
import { CommandHandler } from '@nestjs/cqrs';
|
|
||||||
import { CreateAdCommand } from '../../commands/create-ad.command';
|
|
||||||
import { Ad } from '../entities/ad';
|
|
||||||
import { AdRepository } from '../../adapters/secondaries/ad.repository';
|
|
||||||
import { InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Mapper } from '@automapper/core';
|
|
||||||
import { CreateAdRequest } from '../dtos/create-ad.request';
|
|
||||||
import { Inject } from '@nestjs/common';
|
|
||||||
import { IProvideParams } from '../interfaces/params-provider.interface';
|
|
||||||
import { ICreateGeorouter } from '../../../geography/domain/interfaces/georouter-creator.interface';
|
|
||||||
import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface';
|
|
||||||
import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface';
|
|
||||||
import { DefaultParams } from '../types/default-params.type';
|
|
||||||
import { Role } from '../types/role.enum';
|
|
||||||
import { Geography } from '../entities/geography';
|
|
||||||
import { IEncodeDirection } from '../../../geography/domain/interfaces/direction-encoder.interface';
|
|
||||||
import { TimeConverter } from '../entities/time-converter';
|
|
||||||
import { Coordinate } from '../../../geography/domain/entities/coordinate';
|
|
||||||
|
|
||||||
@CommandHandler(CreateAdCommand)
|
|
||||||
export class CreateAdUseCase {
|
|
||||||
private readonly georouter: IGeorouter;
|
|
||||||
private readonly defaultParams: DefaultParams;
|
|
||||||
private timezone: string;
|
|
||||||
private roles: Role[];
|
|
||||||
private geography: Geography;
|
|
||||||
private ad: Ad;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectMapper() private readonly mapper: Mapper,
|
|
||||||
private readonly adRepository: AdRepository,
|
|
||||||
@Inject('ParamsProvider')
|
|
||||||
private readonly defaultParamsProvider: IProvideParams,
|
|
||||||
@Inject('GeorouterCreator')
|
|
||||||
private readonly georouterCreator: ICreateGeorouter,
|
|
||||||
@Inject('TimezoneFinder')
|
|
||||||
private readonly timezoneFinder: IFindTimezone,
|
|
||||||
@Inject('DirectionEncoder')
|
|
||||||
private readonly directionEncoder: IEncodeDirection,
|
|
||||||
) {
|
|
||||||
this.defaultParams = defaultParamsProvider.getParams();
|
|
||||||
this.georouter = georouterCreator.create(
|
|
||||||
this.defaultParams.GEOROUTER_TYPE,
|
|
||||||
this.defaultParams.GEOROUTER_URL,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(command: CreateAdCommand): Promise<Ad> {
|
|
||||||
try {
|
|
||||||
this.ad = this.mapper.map(command.createAdRequest, CreateAdRequest, Ad);
|
|
||||||
this.setTimezone(command.createAdRequest.waypoints);
|
|
||||||
this.setGeography(command.createAdRequest.waypoints);
|
|
||||||
this.setRoles(command.createAdRequest);
|
|
||||||
await this.geography.createRoutes(this.roles, this.georouter, {
|
|
||||||
withDistance: false,
|
|
||||||
withPoints: true,
|
|
||||||
withTime: false,
|
|
||||||
});
|
|
||||||
this.setAdGeography(command);
|
|
||||||
this.setAdSchedule(command);
|
|
||||||
return await this.adRepository.createAd(this.ad);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setTimezone = (coordinates: Coordinate[]): void => {
|
|
||||||
this.timezone = this.defaultParams.DEFAULT_TIMEZONE;
|
|
||||||
try {
|
|
||||||
const timezones = this.timezoneFinder.timezones(
|
|
||||||
coordinates[0].lon,
|
|
||||||
coordinates[0].lat,
|
|
||||||
);
|
|
||||||
if (timezones.length > 0) this.timezone = timezones[0];
|
|
||||||
} catch (e) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
private setRoles = (createAdRequest: CreateAdRequest): void => {
|
|
||||||
this.roles = [];
|
|
||||||
if (createAdRequest.driver) this.roles.push(Role.DRIVER);
|
|
||||||
if (createAdRequest.passenger) this.roles.push(Role.PASSENGER);
|
|
||||||
};
|
|
||||||
|
|
||||||
private setGeography = (coordinates: Coordinate[]): void => {
|
|
||||||
this.geography = new Geography(coordinates);
|
|
||||||
};
|
|
||||||
|
|
||||||
private setAdGeography = (command: CreateAdCommand): void => {
|
|
||||||
this.ad.driverDistance = this.geography.driverRoute?.distance;
|
|
||||||
this.ad.driverDuration = this.geography.driverRoute?.duration;
|
|
||||||
this.ad.passengerDistance = this.geography.passengerRoute?.distance;
|
|
||||||
this.ad.passengerDuration = this.geography.passengerRoute?.duration;
|
|
||||||
this.ad.fwdAzimuth = this.geography.driverRoute
|
|
||||||
? this.geography.driverRoute.fwdAzimuth
|
|
||||||
: this.geography.passengerRoute.fwdAzimuth;
|
|
||||||
this.ad.backAzimuth = this.geography.driverRoute
|
|
||||||
? this.geography.driverRoute.backAzimuth
|
|
||||||
: this.geography.passengerRoute.backAzimuth;
|
|
||||||
this.ad.waypoints = this.directionEncoder.encode(
|
|
||||||
command.createAdRequest.waypoints,
|
|
||||||
);
|
|
||||||
this.ad.direction = this.geography.driverRoute
|
|
||||||
? this.directionEncoder.encode(this.geography.driverRoute.points)
|
|
||||||
: undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
private setAdSchedule = (command: CreateAdCommand): void => {
|
|
||||||
this.ad.monTime = TimeConverter.toUtcDatetime(
|
|
||||||
this.ad.fromDate,
|
|
||||||
command.createAdRequest.monTime,
|
|
||||||
this.timezone,
|
|
||||||
);
|
|
||||||
this.ad.tueTime = TimeConverter.toUtcDatetime(
|
|
||||||
this.ad.fromDate,
|
|
||||||
command.createAdRequest.tueTime,
|
|
||||||
this.timezone,
|
|
||||||
);
|
|
||||||
this.ad.wedTime = TimeConverter.toUtcDatetime(
|
|
||||||
this.ad.fromDate,
|
|
||||||
command.createAdRequest.wedTime,
|
|
||||||
this.timezone,
|
|
||||||
);
|
|
||||||
this.ad.thuTime = TimeConverter.toUtcDatetime(
|
|
||||||
this.ad.fromDate,
|
|
||||||
command.createAdRequest.thuTime,
|
|
||||||
this.timezone,
|
|
||||||
);
|
|
||||||
this.ad.friTime = TimeConverter.toUtcDatetime(
|
|
||||||
this.ad.fromDate,
|
|
||||||
command.createAdRequest.friTime,
|
|
||||||
this.timezone,
|
|
||||||
);
|
|
||||||
this.ad.satTime = TimeConverter.toUtcDatetime(
|
|
||||||
this.ad.fromDate,
|
|
||||||
command.createAdRequest.satTime,
|
|
||||||
this.timezone,
|
|
||||||
);
|
|
||||||
this.ad.sunTime = TimeConverter.toUtcDatetime(
|
|
||||||
this.ad.fromDate,
|
|
||||||
command.createAdRequest.sunTime,
|
|
||||||
this.timezone,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||||
|
import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
||||||
|
import { AdEntity } from '../core/domain/ad.entity';
|
||||||
|
import { AdMapper } from '../ad.mapper';
|
||||||
|
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
|
||||||
|
|
||||||
|
export type AdBaseModel = {
|
||||||
|
uuid: string;
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: string;
|
||||||
|
fromDate: Date;
|
||||||
|
toDate: Date;
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
driverDuration: number;
|
||||||
|
driverDistance: number;
|
||||||
|
passengerDuration: number;
|
||||||
|
passengerDistance: number;
|
||||||
|
fwdAzimuth: number;
|
||||||
|
backAzimuth: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdReadModel = AdBaseModel & {
|
||||||
|
waypoints: string;
|
||||||
|
direction: string;
|
||||||
|
schedule: ScheduleItemModel[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdWriteModel = AdBaseModel & {
|
||||||
|
schedule: {
|
||||||
|
create: ScheduleItemModel[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdUnsupportedWriteModel = {
|
||||||
|
waypoints: string;
|
||||||
|
direction: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScheduleItemModel = {
|
||||||
|
uuid: string;
|
||||||
|
day: number;
|
||||||
|
time: Date;
|
||||||
|
margin: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository is used for retrieving/saving domain entities
|
||||||
|
* */
|
||||||
|
@Injectable()
|
||||||
|
export class AdRepository
|
||||||
|
extends ExtendedPrismaRepositoryBase<
|
||||||
|
AdEntity,
|
||||||
|
AdReadModel,
|
||||||
|
AdWriteModel,
|
||||||
|
AdUnsupportedWriteModel
|
||||||
|
>
|
||||||
|
implements AdRepositoryPort
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
prisma: PrismaService,
|
||||||
|
mapper: AdMapper,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
@Inject(AD_MESSAGE_PUBLISHER)
|
||||||
|
protected readonly messagePublisher: MessagePublisherPort,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
prisma.ad,
|
||||||
|
prisma,
|
||||||
|
mapper,
|
||||||
|
eventEmitter,
|
||||||
|
new LoggerBase({
|
||||||
|
logger: new Logger(AdRepository.name),
|
||||||
|
domain: 'matcher',
|
||||||
|
messagePublisher,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { RouteProviderPort } from '../core/application/ports/route-provider.port';
|
||||||
|
import { Route } from '../core/application/types/route.type';
|
||||||
|
import { Waypoint } from '../core/application/types/waypoint.type';
|
||||||
|
import { Role } from '../core/domain/ad.types';
|
||||||
|
import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port';
|
||||||
|
import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RouteProvider implements RouteProviderPort {
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_GET_BASIC_ROUTE_CONTROLLER)
|
||||||
|
private readonly getBasicRouteController: GetBasicRouteControllerPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getBasic = async (roles: Role[], waypoints: Waypoint[]): Promise<Route> =>
|
||||||
|
await this.getBasicRouteController.get({
|
||||||
|
roles,
|
||||||
|
waypoints,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||||
|
import { Ad } from './ad.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdCreatedMessageHandler {
|
||||||
|
constructor(private readonly commandBus: CommandBus) {}
|
||||||
|
|
||||||
|
@RabbitSubscribe({
|
||||||
|
name: 'adCreated',
|
||||||
|
})
|
||||||
|
public async adCreated(message: string) {
|
||||||
|
try {
|
||||||
|
const createdAd: Ad = JSON.parse(message);
|
||||||
|
await this.commandBus.execute(
|
||||||
|
new CreateAdCommand({
|
||||||
|
id: createdAd.id,
|
||||||
|
driver: createdAd.driver,
|
||||||
|
passenger: createdAd.passenger,
|
||||||
|
frequency: createdAd.frequency,
|
||||||
|
fromDate: createdAd.fromDate,
|
||||||
|
toDate: createdAd.toDate,
|
||||||
|
schedule: createdAd.schedule,
|
||||||
|
seatsProposed: createdAd.seatsProposed,
|
||||||
|
seatsRequested: createdAd.seatsRequested,
|
||||||
|
strict: createdAd.strict,
|
||||||
|
waypoints: createdAd.waypoints,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (e: any) {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
|
||||||
|
export type Ad = {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: Frequency;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
schedule: ScheduleItem[];
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
waypoints: Waypoint[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScheduleItem = {
|
||||||
|
day: number;
|
||||||
|
time: string;
|
||||||
|
margin: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Waypoint = {
|
||||||
|
position: number;
|
||||||
|
name?: string;
|
||||||
|
houseNumber?: string;
|
||||||
|
street?: string;
|
||||||
|
locality?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country: string;
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
};
|
|
@ -1,18 +0,0 @@
|
||||||
import { createMap, Mapper } from '@automapper/core';
|
|
||||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Ad } from '../domain/entities/ad';
|
|
||||||
import { CreateAdRequest } from '../domain/dtos/create-ad.request';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AdProfile extends AutomapperProfile {
|
|
||||||
constructor(@InjectMapper() mapper: Mapper) {
|
|
||||||
super(mapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
override get profile() {
|
|
||||||
return (mapper: any) => {
|
|
||||||
createMap(mapper, CreateAdRequest, Ad);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,218 +1,61 @@
|
||||||
import { TestingModule, Test } from '@nestjs/testing';
|
import {
|
||||||
import { DatabaseModule } from '../../../database/database.module';
|
AD_DIRECTION_ENCODER,
|
||||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma.service';
|
AD_MESSAGE_PUBLISHER,
|
||||||
import { AdRepository } from '../../adapters/secondaries/ad.repository';
|
AD_REPOSITORY,
|
||||||
import { Ad } from '../../domain/entities/ad';
|
} from '@modules/ad/ad.di-tokens';
|
||||||
import { Frequency } from '../../domain/types/frequency.enum';
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
|
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
|
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
describe('AdRepository', () => {
|
describe('Ad Repository', () => {
|
||||||
let prismaService: PrismaService;
|
let prismaService: PrismaService;
|
||||||
let adRepository: AdRepository;
|
let adRepository: AdRepository;
|
||||||
|
|
||||||
const baseUuid = {
|
const mockMessagePublisher = {
|
||||||
uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00',
|
publish: jest.fn().mockImplementation(),
|
||||||
};
|
|
||||||
const baseUserUuid = {
|
|
||||||
userUuid: '4e52b54d-a729-4dbd-9283-f84a11bb2200',
|
|
||||||
};
|
|
||||||
const driverAd = {
|
|
||||||
driver: 'true',
|
|
||||||
passenger: 'false',
|
|
||||||
fwdAzimuth: 0,
|
|
||||||
backAzimuth: 180,
|
|
||||||
waypoints: "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'",
|
|
||||||
direction: "'LINESTRING(6 47,6.05 47.05,6.1 47.1,6.15 47.15,6.2 47.2)'",
|
|
||||||
seatsDriver: 3,
|
|
||||||
seatsPassenger: 1,
|
|
||||||
seatsUsed: 0,
|
|
||||||
strict: 'false',
|
|
||||||
};
|
|
||||||
const passengerAd = {
|
|
||||||
driver: 'false',
|
|
||||||
passenger: 'true',
|
|
||||||
fwdAzimuth: 0,
|
|
||||||
backAzimuth: 180,
|
|
||||||
waypoints: "'LINESTRING(6 47,6.2 47.2)'",
|
|
||||||
direction: "'LINESTRING(6 47,6.05 47.05,6.15 47.15,6.2 47.2)'",
|
|
||||||
seatsDriver: 3,
|
|
||||||
seatsPassenger: 1,
|
|
||||||
seatsUsed: 0,
|
|
||||||
strict: 'false',
|
|
||||||
};
|
|
||||||
const driverAndPassengerAd = {
|
|
||||||
driver: 'true',
|
|
||||||
passenger: 'true',
|
|
||||||
fwdAzimuth: 0,
|
|
||||||
backAzimuth: 180,
|
|
||||||
waypoints: "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'",
|
|
||||||
direction: "'LINESTRING(6 47,6.05 47.05,6.1 47.1,6.15 47.15,6.2 47.2)'",
|
|
||||||
seatsDriver: 3,
|
|
||||||
seatsPassenger: 1,
|
|
||||||
seatsUsed: 0,
|
|
||||||
strict: 'false',
|
|
||||||
};
|
|
||||||
const punctualAd = {
|
|
||||||
frequency: `'PUNCTUAL'`,
|
|
||||||
fromDate: `'2023-01-01'`,
|
|
||||||
toDate: `'2023-01-01'`,
|
|
||||||
monTime: 'NULL',
|
|
||||||
tueTime: 'NULL',
|
|
||||||
wedTime: 'NULL',
|
|
||||||
thuTime: 'NULL',
|
|
||||||
friTime: 'NULL',
|
|
||||||
satTime: 'NULL',
|
|
||||||
sunTime: `'2023-01-01T07:00Z'`,
|
|
||||||
monMargin: 900,
|
|
||||||
tueMargin: 900,
|
|
||||||
wedMargin: 900,
|
|
||||||
thuMargin: 900,
|
|
||||||
friMargin: 900,
|
|
||||||
satMargin: 900,
|
|
||||||
sunMargin: 900,
|
|
||||||
};
|
|
||||||
const recurrentAd = {
|
|
||||||
frequency: `'RECURRENT'`,
|
|
||||||
fromDate: `'2023-01-01'`,
|
|
||||||
toDate: `'2023-12-31'`,
|
|
||||||
monTime: `'2023-01-01T07:00Z'`,
|
|
||||||
tueTime: `'2023-01-01T07:00Z'`,
|
|
||||||
wedTime: `'2023-01-01T07:00Z'`,
|
|
||||||
thuTime: `'2023-01-01T07:00Z'`,
|
|
||||||
friTime: `'2023-01-01T07:00Z'`,
|
|
||||||
satTime: 'NULL',
|
|
||||||
sunTime: 'NULL',
|
|
||||||
monMargin: 900,
|
|
||||||
tueMargin: 900,
|
|
||||||
wedMargin: 900,
|
|
||||||
thuMargin: 900,
|
|
||||||
friMargin: 900,
|
|
||||||
satMargin: 900,
|
|
||||||
sunMargin: 900,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPunctualDriverAds = async (nbToCreate = 10) => {
|
const mockLogger = {
|
||||||
const adToCreate = {
|
log: jest.fn(),
|
||||||
...baseUuid,
|
warn: jest.fn(),
|
||||||
...baseUserUuid,
|
error: jest.fn(),
|
||||||
...driverAd,
|
|
||||||
...punctualAd,
|
|
||||||
};
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
await executeInsertCommand(adToCreate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRecurrentDriverAds = async (nbToCreate = 10) => {
|
|
||||||
const adToCreate = {
|
|
||||||
...baseUuid,
|
|
||||||
...baseUserUuid,
|
|
||||||
...driverAd,
|
|
||||||
...recurrentAd,
|
|
||||||
};
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
await executeInsertCommand(adToCreate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPunctualPassengerAds = async (nbToCreate = 10) => {
|
|
||||||
const adToCreate = {
|
|
||||||
...baseUuid,
|
|
||||||
...baseUserUuid,
|
|
||||||
...passengerAd,
|
|
||||||
...punctualAd,
|
|
||||||
};
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
await executeInsertCommand(adToCreate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRecurrentPassengerAds = async (nbToCreate = 10) => {
|
|
||||||
const adToCreate = {
|
|
||||||
...baseUuid,
|
|
||||||
...baseUserUuid,
|
|
||||||
...passengerAd,
|
|
||||||
...recurrentAd,
|
|
||||||
};
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
await executeInsertCommand(adToCreate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPunctualDriverPassengerAds = async (nbToCreate = 10) => {
|
|
||||||
const adToCreate = {
|
|
||||||
...baseUuid,
|
|
||||||
...baseUserUuid,
|
|
||||||
...driverAndPassengerAd,
|
|
||||||
...punctualAd,
|
|
||||||
};
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
await executeInsertCommand(adToCreate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createRecurrentDriverPassengerAds = async (nbToCreate = 10) => {
|
|
||||||
const adToCreate = {
|
|
||||||
...baseUuid,
|
|
||||||
...baseUserUuid,
|
|
||||||
...driverAndPassengerAd,
|
|
||||||
...recurrentAd,
|
|
||||||
};
|
|
||||||
for (let i = 0; i < nbToCreate; i++) {
|
|
||||||
adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, '0')}'`;
|
|
||||||
await executeInsertCommand(adToCreate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const executeInsertCommand = async (object: any) => {
|
|
||||||
const command = `INSERT INTO ad ("${Object.keys(object).join(
|
|
||||||
'","',
|
|
||||||
)}") VALUES (${Object.values(object).join(',')})`;
|
|
||||||
await prismaService.$executeRawUnsafe(command);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module = await Test.createTestingModule({
|
||||||
imports: [DatabaseModule],
|
imports: [
|
||||||
providers: [AdRepository, PrismaService],
|
EventEmitterModule.forRoot(),
|
||||||
}).compile();
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
PrismaService,
|
||||||
|
AdMapper,
|
||||||
|
{
|
||||||
|
provide: AD_REPOSITORY,
|
||||||
|
useClass: AdRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_MESSAGE_PUBLISHER,
|
||||||
|
useValue: mockMessagePublisher,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_DIRECTION_ENCODER,
|
||||||
|
useClass: PostgresDirectionEncoder,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
// disable logging
|
||||||
|
.setLogger(mockLogger)
|
||||||
|
.compile();
|
||||||
|
|
||||||
prismaService = module.get<PrismaService>(PrismaService);
|
prismaService = module.get<PrismaService>(PrismaService);
|
||||||
adRepository = module.get<AdRepository>(AdRepository);
|
adRepository = module.get<AdRepository>(AD_REPOSITORY);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -223,180 +66,157 @@ describe('AdRepository', () => {
|
||||||
await prismaService.ad.deleteMany();
|
await prismaService.ad.deleteMany();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findAll', () => {
|
|
||||||
it('should return an empty data array', async () => {
|
|
||||||
const res = await adRepository.findAll();
|
|
||||||
expect(res).toEqual({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('drivers', () => {
|
|
||||||
it('should return a data array with 8 punctual driver ads', async () => {
|
|
||||||
await createPunctualDriverAds(8);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(8);
|
|
||||||
expect(ads.total).toBe(8);
|
|
||||||
expect(ads.data[0].driver).toBeTruthy();
|
|
||||||
expect(ads.data[0].passenger).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 punctual driver ads', async () => {
|
|
||||||
await createPunctualDriverAds(20);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(10);
|
|
||||||
expect(ads.total).toBe(20);
|
|
||||||
expect(ads.data[1].driver).toBeTruthy();
|
|
||||||
expect(ads.data[1].passenger).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array with 8 recurrent driver ads', async () => {
|
|
||||||
await createRecurrentDriverAds(8);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(8);
|
|
||||||
expect(ads.total).toBe(8);
|
|
||||||
expect(ads.data[2].driver).toBeTruthy();
|
|
||||||
expect(ads.data[2].passenger).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 recurrent driver ads', async () => {
|
|
||||||
await createRecurrentDriverAds(20);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(10);
|
|
||||||
expect(ads.total).toBe(20);
|
|
||||||
expect(ads.data[3].driver).toBeTruthy();
|
|
||||||
expect(ads.data[3].passenger).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('passengers', () => {
|
|
||||||
it('should return a data array with 7 punctual passenger ads', async () => {
|
|
||||||
await createPunctualPassengerAds(7);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(7);
|
|
||||||
expect(ads.total).toBe(7);
|
|
||||||
expect(ads.data[0].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[0].driver).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 punctual passenger ads', async () => {
|
|
||||||
await createPunctualPassengerAds(15);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(10);
|
|
||||||
expect(ads.total).toBe(15);
|
|
||||||
expect(ads.data[1].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[1].driver).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array with 7 recurrent passenger ads', async () => {
|
|
||||||
await createRecurrentPassengerAds(7);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(7);
|
|
||||||
expect(ads.total).toBe(7);
|
|
||||||
expect(ads.data[2].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[2].driver).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 recurrent passenger ads', async () => {
|
|
||||||
await createRecurrentPassengerAds(15);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(10);
|
|
||||||
expect(ads.total).toBe(15);
|
|
||||||
expect(ads.data[3].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[3].driver).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('drivers and passengers', () => {
|
|
||||||
it('should return a data array with 6 punctual driver and passenger ads', async () => {
|
|
||||||
await createPunctualDriverPassengerAds(6);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(6);
|
|
||||||
expect(ads.total).toBe(6);
|
|
||||||
expect(ads.data[0].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[0].driver).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 punctual driver and passenger ads', async () => {
|
|
||||||
await createPunctualDriverPassengerAds(16);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(10);
|
|
||||||
expect(ads.total).toBe(16);
|
|
||||||
expect(ads.data[1].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[1].driver).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array with 6 recurrent driver and passenger ads', async () => {
|
|
||||||
await createRecurrentDriverPassengerAds(6);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(6);
|
|
||||||
expect(ads.total).toBe(6);
|
|
||||||
expect(ads.data[2].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[2].driver).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a data array limited to 10 recurrent driver and passenger ads', async () => {
|
|
||||||
await createRecurrentDriverPassengerAds(16);
|
|
||||||
const ads = await adRepository.findAll();
|
|
||||||
expect(ads.data.length).toBe(10);
|
|
||||||
expect(ads.total).toBe(16);
|
|
||||||
expect(ads.data[3].passenger).toBeTruthy();
|
|
||||||
expect(ads.data[3].driver).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOneByUuid', () => {
|
|
||||||
it('should return an ad', async () => {
|
|
||||||
await createPunctualDriverAds(1);
|
|
||||||
const ad = await adRepository.findOneByUuid(baseUuid.uuid);
|
|
||||||
expect(ad.uuid).toBe(baseUuid.uuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null', async () => {
|
|
||||||
const ad = await adRepository.findOneByUuid(
|
|
||||||
'544572be-11fb-4244-8235-587221fc9104',
|
|
||||||
);
|
|
||||||
expect(ad).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('should create an ad', async () => {
|
it('should create a punctual ad', async () => {
|
||||||
const beforeCount = await prismaService.ad.count();
|
const beforeCount = await prismaService.ad.count();
|
||||||
|
|
||||||
const adToCreate: Ad = new Ad();
|
const createAdProps: CreateAdProps = {
|
||||||
adToCreate.uuid = 'be459a29-7a41-4c0b-b371-abe90bfb6f00';
|
id: 'b4b56444-f8d3-4110-917c-e37bba77f383',
|
||||||
adToCreate.userUuid = '4e52b54d-a729-4dbd-9283-f84a11bb2200';
|
driver: true,
|
||||||
adToCreate.driver = true;
|
passenger: false,
|
||||||
adToCreate.passenger = false;
|
frequency: Frequency.PUNCTUAL,
|
||||||
adToCreate.fwdAzimuth = 0;
|
fromDate: '2023-02-01',
|
||||||
adToCreate.backAzimuth = 180;
|
toDate: '2023-02-01',
|
||||||
adToCreate.waypoints = "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'";
|
schedule: [
|
||||||
adToCreate.direction =
|
{
|
||||||
"'LINESTRING(6 47,6.05 47.05,6.1 47.1,6.15 47.15,6.2 47.2)'";
|
day: 3,
|
||||||
adToCreate.seatsDriver = 3;
|
time: '12:05',
|
||||||
adToCreate.seatsPassenger = 1;
|
margin: 900,
|
||||||
adToCreate.seatsUsed = 0;
|
},
|
||||||
adToCreate.strict = false;
|
],
|
||||||
adToCreate.frequency = Frequency.PUNCTUAL;
|
seatsProposed: 3,
|
||||||
adToCreate.fromDate = new Date(2023, 0, 1);
|
seatsRequested: 1,
|
||||||
adToCreate.toDate = new Date(2023, 0, 1);
|
strict: false,
|
||||||
adToCreate.sunTime = new Date(2023, 0, 1, 6, 0, 0);
|
waypoints: [
|
||||||
adToCreate.monMargin = 900;
|
{
|
||||||
adToCreate.tueMargin = 900;
|
position: 0,
|
||||||
adToCreate.wedMargin = 900;
|
lon: 43.7102,
|
||||||
adToCreate.thuMargin = 900;
|
lat: 7.262,
|
||||||
adToCreate.friMargin = 900;
|
},
|
||||||
adToCreate.satMargin = 900;
|
{
|
||||||
adToCreate.sunMargin = 900;
|
position: 1,
|
||||||
const ad = await adRepository.createAd(adToCreate);
|
lon: 43.2965,
|
||||||
|
lat: 5.3698,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 7.262,
|
||||||
|
lat: 43.7102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 6.797838,
|
||||||
|
lat: 43.547031,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 6.18535,
|
||||||
|
lat: 43.407517,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 5.3698,
|
||||||
|
lat: 43.2965,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
driverDuration: 7668,
|
||||||
|
driverDistance: 199000,
|
||||||
|
passengerDuration: 7668,
|
||||||
|
passengerDistance: 199000,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
};
|
||||||
|
|
||||||
|
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
||||||
|
await adRepository.insertWithUnsupportedFields(adToCreate, 'ad');
|
||||||
|
|
||||||
|
const afterCount = await prismaService.ad.count();
|
||||||
|
|
||||||
|
expect(afterCount - beforeCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a recurrent ad', async () => {
|
||||||
|
const beforeCount = await prismaService.ad.count();
|
||||||
|
|
||||||
|
const createAdProps: CreateAdProps = {
|
||||||
|
id: 'b4b56444-f8d3-4110-917c-e37bba77f383',
|
||||||
|
driver: true,
|
||||||
|
passenger: false,
|
||||||
|
frequency: Frequency.RECURRENT,
|
||||||
|
fromDate: '2023-02-01',
|
||||||
|
toDate: '2024-01-31',
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
day: 1,
|
||||||
|
time: '08:00',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 2,
|
||||||
|
time: '08:00',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 3,
|
||||||
|
time: '09:00',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 4,
|
||||||
|
time: '08:00',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: 5,
|
||||||
|
time: '08:00',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
seatsProposed: 3,
|
||||||
|
seatsRequested: 1,
|
||||||
|
strict: false,
|
||||||
|
waypoints: [
|
||||||
|
{
|
||||||
|
position: 0,
|
||||||
|
lon: 43.7102,
|
||||||
|
lat: 7.262,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
lon: 43.2965,
|
||||||
|
lat: 5.3698,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 7.262,
|
||||||
|
lat: 43.7102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 6.797838,
|
||||||
|
lat: 43.547031,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 6.18535,
|
||||||
|
lat: 43.407517,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 5.3698,
|
||||||
|
lat: 43.2965,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
driverDuration: 7668,
|
||||||
|
driverDistance: 199000,
|
||||||
|
passengerDuration: 7668,
|
||||||
|
passengerDistance: 199000,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
};
|
||||||
|
|
||||||
|
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
||||||
|
await adRepository.insertWithUnsupportedFields(adToCreate, 'ad');
|
||||||
|
|
||||||
const afterCount = await prismaService.ad.count();
|
const afterCount = await prismaService.ad.count();
|
||||||
|
|
||||||
expect(afterCount - beforeCount).toBe(1);
|
expect(afterCount - beforeCount).toBe(1);
|
||||||
expect(ad.uuid).toBe('be459a29-7a41-4c0b-b371-abe90bfb6f00');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { AD_DIRECTION_ENCODER } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import {
|
||||||
|
AdReadModel,
|
||||||
|
AdUnsupportedWriteModel,
|
||||||
|
AdWriteModel,
|
||||||
|
} from '@modules/ad/infrastructure/ad.repository';
|
||||||
|
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const now = new Date('2023-06-21 06:00:00');
|
||||||
|
const adEntity: AdEntity = new AdEntity({
|
||||||
|
id: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||||
|
props: {
|
||||||
|
driver: true,
|
||||||
|
passenger: true,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
fromDate: '2023-06-21',
|
||||||
|
toDate: '2023-06-21',
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
day: 3,
|
||||||
|
time: '07:15',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
waypoints: [
|
||||||
|
{
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.1765102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
lat: 48.8566,
|
||||||
|
lon: 2.3522,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
strict: false,
|
||||||
|
seatsProposed: 3,
|
||||||
|
seatsRequested: 1,
|
||||||
|
driverDistance: 350101,
|
||||||
|
driverDuration: 14422,
|
||||||
|
passengerDistance: 350101,
|
||||||
|
passengerDuration: 14422,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 6.1765102,
|
||||||
|
lat: 48.689445,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 4.984578,
|
||||||
|
lat: 48.725687,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 2.3522,
|
||||||
|
lat: 48.8566,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const adReadModel: AdReadModel = {
|
||||||
|
uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
|
||||||
|
driver: true,
|
||||||
|
passenger: true,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
fromDate: new Date('2023-06-21'),
|
||||||
|
toDate: new Date('2023-06-21'),
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
uuid: '3978f3d6-560f-4a8f-83ba-9bf5aa9a2d27',
|
||||||
|
day: 3,
|
||||||
|
time: new Date('2023-06-21T07:05:00Z'),
|
||||||
|
margin: 900,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
waypoints: "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'",
|
||||||
|
direction:
|
||||||
|
"'LINESTRING(6.1765102 48.689445,5.12345 48.76543,2.3522 48.8566)'",
|
||||||
|
driverDistance: 350000,
|
||||||
|
driverDuration: 14400,
|
||||||
|
passengerDistance: 350000,
|
||||||
|
passengerDuration: 14400,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
strict: false,
|
||||||
|
seatsProposed: 3,
|
||||||
|
seatsRequested: 1,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDirectionEncoder: DirectionEncoderPort = {
|
||||||
|
encode: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(
|
||||||
|
() => "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'",
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(
|
||||||
|
() =>
|
||||||
|
"'LINESTRING(6.1765102 48.689445,4.984578 48.725687,2.3522 48.8566)'",
|
||||||
|
),
|
||||||
|
decode: jest.fn().mockImplementation(() => [
|
||||||
|
{
|
||||||
|
lon: 6.1765102,
|
||||||
|
lat: 48.689445,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 2.3522,
|
||||||
|
lat: 48.8566,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Ad Mapper', () => {
|
||||||
|
let adMapper: AdMapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AdMapper,
|
||||||
|
{
|
||||||
|
provide: AD_DIRECTION_ENCODER,
|
||||||
|
useValue: mockDirectionEncoder,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
adMapper = module.get<AdMapper>(AdMapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(adMapper).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map domain entity to persistence data', async () => {
|
||||||
|
const mapped: AdWriteModel = adMapper.toPersistence(adEntity);
|
||||||
|
expect(mapped.schedule.create.length).toBe(1);
|
||||||
|
expect(mapped.driverDuration).toBe(14422);
|
||||||
|
expect(mapped.fwdAzimuth).toBe(273);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map domain entity to unsupported db persistence data', async () => {
|
||||||
|
const mapped: AdUnsupportedWriteModel =
|
||||||
|
adMapper.toUnsupportedPersistence(adEntity);
|
||||||
|
expect(mapped.waypoints).toBe(
|
||||||
|
"'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'",
|
||||||
|
);
|
||||||
|
expect(mapped.direction).toBe(
|
||||||
|
"'LINESTRING(6.1765102 48.689445,4.984578 48.725687,2.3522 48.8566)'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map persisted data to domain entity', async () => {
|
||||||
|
const mapped: AdEntity = adMapper.toDomain(adReadModel);
|
||||||
|
expect(mapped.getProps().schedule.length).toBe(1);
|
||||||
|
expect(mapped.getProps().schedule[0].time).toBe('07:05');
|
||||||
|
expect(mapped.getProps().waypoints.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map domain entity to response', async () => {
|
||||||
|
expect(adMapper.toResponse(adEntity)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,47 +0,0 @@
|
||||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { Messager } from '../../../../adapters/secondaries/messager';
|
|
||||||
|
|
||||||
const mockAmqpConnection = {
|
|
||||||
publish: jest.fn().mockImplementation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockConfigService = {
|
|
||||||
get: jest.fn().mockResolvedValue({
|
|
||||||
RMQ_EXCHANGE: 'mobicoop',
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Messager', () => {
|
|
||||||
let messager: Messager;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [
|
|
||||||
Messager,
|
|
||||||
{
|
|
||||||
provide: AmqpConnection,
|
|
||||||
useValue: mockAmqpConnection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ConfigService,
|
|
||||||
useValue: mockConfigService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
messager = module.get<Messager>(Messager);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(messager).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should publish a message', async () => {
|
|
||||||
jest.spyOn(mockAmqpConnection, 'publish');
|
|
||||||
messager.publish('test.create.info', 'my-test');
|
|
||||||
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { TimezoneFinder } from '../../../../adapters/secondaries/timezone-finder';
|
|
||||||
import { GeoTimezoneFinder } from '../../../../../geography/adapters/secondaries/geo-timezone-finder';
|
|
||||||
|
|
||||||
const mockGeoTimezoneFinder = {
|
|
||||||
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Timezone Finder', () => {
|
|
||||||
let timezoneFinder: TimezoneFinder;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [],
|
|
||||||
providers: [
|
|
||||||
TimezoneFinder,
|
|
||||||
{
|
|
||||||
provide: GeoTimezoneFinder,
|
|
||||||
useValue: mockGeoTimezoneFinder,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
timezoneFinder = module.get<TimezoneFinder>(TimezoneFinder);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(timezoneFinder).toBeDefined();
|
|
||||||
});
|
|
||||||
it('should get timezone for Nancy(France) as Europe/Paris', () => {
|
|
||||||
const timezones = timezoneFinder.timezones(6.179373, 48.687913);
|
|
||||||
expect(timezones.length).toBe(1);
|
|
||||||
expect(timezones[0]).toBe('Europe/Paris');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
const originWaypointProps: WaypointProps = {
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
};
|
||||||
|
const destinationWaypointProps: WaypointProps = {
|
||||||
|
position: 1,
|
||||||
|
lat: 48.8566,
|
||||||
|
lon: 2.3522,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAdProps: CreateAdProps = {
|
||||||
|
id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
|
||||||
|
driver: true,
|
||||||
|
passenger: true,
|
||||||
|
fromDate: '2023-06-21',
|
||||||
|
toDate: '2023-06-21',
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
day: 3,
|
||||||
|
time: '08:30',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
seatsProposed: 3,
|
||||||
|
seatsRequested: 1,
|
||||||
|
strict: false,
|
||||||
|
waypoints: [originWaypointProps, destinationWaypointProps],
|
||||||
|
driverDistance: 23000,
|
||||||
|
driverDuration: 900,
|
||||||
|
passengerDistance: 23000,
|
||||||
|
passengerDuration: 900,
|
||||||
|
fwdAzimuth: 283,
|
||||||
|
backAzimuth: 93,
|
||||||
|
points: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Ad entity create', () => {
|
||||||
|
it('should create a new entity', async () => {
|
||||||
|
const ad: AdEntity = AdEntity.create(createAdProps);
|
||||||
|
expect(ad.id.length).toBe(36);
|
||||||
|
expect(ad.getProps().schedule.length).toBe(1);
|
||||||
|
expect(ad.getProps().schedule[0].day).toBe(3);
|
||||||
|
expect(ad.getProps().schedule[0].time).toBe('08:30');
|
||||||
|
expect(ad.getProps().driver).toBeTruthy();
|
||||||
|
expect(ad.getProps().passenger).toBeTruthy();
|
||||||
|
expect(ad.getProps().driverDistance).toBe(23000);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
|
import { ConflictException } from '@mobicoop/ddd-library';
|
||||||
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
|
||||||
|
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
|
||||||
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
|
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||||
|
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||||
|
|
||||||
|
const originWaypoint: WaypointProps = {
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
};
|
||||||
|
const destinationWaypoint: WaypointProps = {
|
||||||
|
position: 1,
|
||||||
|
lat: 48.8566,
|
||||||
|
lon: 2.3522,
|
||||||
|
};
|
||||||
|
const createAdProps: CreateAdProps = {
|
||||||
|
id: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
|
||||||
|
fromDate: '2023-12-21',
|
||||||
|
toDate: '2023-12-21',
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
day: 4,
|
||||||
|
time: '08:15',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
driver: true,
|
||||||
|
passenger: true,
|
||||||
|
seatsProposed: 3,
|
||||||
|
seatsRequested: 1,
|
||||||
|
strict: false,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
|
driverDistance: 23000,
|
||||||
|
driverDuration: 900,
|
||||||
|
passengerDistance: 23000,
|
||||||
|
passengerDuration: 900,
|
||||||
|
fwdAzimuth: 283,
|
||||||
|
backAzimuth: 93,
|
||||||
|
points: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockAdRepository = {
|
||||||
|
insertWithUnsupportedFields: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(() => ({}))
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error();
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new ConflictException('already exists');
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRouteProvider: RouteProviderPort = {
|
||||||
|
getBasic: jest.fn().mockImplementation(() => ({
|
||||||
|
driverDistance: 350101,
|
||||||
|
driverDuration: 14422,
|
||||||
|
passengerDistance: 350101,
|
||||||
|
passengerDuration: 14422,
|
||||||
|
fwdAzimuth: 273,
|
||||||
|
backAzimuth: 93,
|
||||||
|
distanceAzimuth: 336544,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 6.1765102,
|
||||||
|
lat: 48.689445,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 4.984578,
|
||||||
|
lat: 48.725687,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 2.3522,
|
||||||
|
lat: 48.8566,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('create-ad.service', () => {
|
||||||
|
let createAdService: CreateAdService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: AD_REPOSITORY,
|
||||||
|
useValue: mockAdRepository,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_ROUTE_PROVIDER,
|
||||||
|
useValue: mockRouteProvider,
|
||||||
|
},
|
||||||
|
CreateAdService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
createAdService = module.get<CreateAdService>(CreateAdService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(createAdService).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execution', () => {
|
||||||
|
const createAdCommand = new CreateAdCommand(createAdProps);
|
||||||
|
it('should create a new ad', async () => {
|
||||||
|
AdEntity.create = jest.fn().mockReturnValue({
|
||||||
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
|
});
|
||||||
|
const result: AggregateID = await createAdService.execute(
|
||||||
|
createAdCommand,
|
||||||
|
);
|
||||||
|
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
||||||
|
});
|
||||||
|
it('should throw an error if something bad happens', async () => {
|
||||||
|
AdEntity.create = jest.fn().mockReturnValue({
|
||||||
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
createAdService.execute(createAdCommand),
|
||||||
|
).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
it('should throw an exception if Ad already exists', async () => {
|
||||||
|
AdEntity.create = jest.fn().mockReturnValue({
|
||||||
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
createAdService.execute(createAdCommand),
|
||||||
|
).rejects.toBeInstanceOf(AdAlreadyExistsException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
|
||||||
|
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
|
|
||||||
|
describe('Point value object', () => {
|
||||||
|
it('should create a point value object', () => {
|
||||||
|
const pointVO = new Point({
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
});
|
||||||
|
expect(pointVO.lat).toBe(48.689445);
|
||||||
|
expect(pointVO.lon).toBe(6.17651);
|
||||||
|
});
|
||||||
|
it('should throw an exception if longitude is invalid', () => {
|
||||||
|
try {
|
||||||
|
new Point({
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 186.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new Point({
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: -186.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should throw an exception if latitude is invalid', () => {
|
||||||
|
try {
|
||||||
|
new Point({
|
||||||
|
lat: 148.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new Point({
|
||||||
|
lat: -148.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {
|
||||||
|
ArgumentInvalidException,
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
|
||||||
|
|
||||||
|
describe('Schedule item value object', () => {
|
||||||
|
it('should create a schedule item value object', () => {
|
||||||
|
const scheduleItemVO = new ScheduleItem({
|
||||||
|
day: 0,
|
||||||
|
time: '07:00',
|
||||||
|
margin: 900,
|
||||||
|
});
|
||||||
|
expect(scheduleItemVO.day).toBe(0);
|
||||||
|
expect(scheduleItemVO.time).toBe('07:00');
|
||||||
|
expect(scheduleItemVO.margin).toBe(900);
|
||||||
|
});
|
||||||
|
it('should throw an exception if day is invalid', () => {
|
||||||
|
try {
|
||||||
|
new ScheduleItem({
|
||||||
|
day: 7,
|
||||||
|
time: '07:00',
|
||||||
|
margin: 900,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should throw an exception if time is invalid', () => {
|
||||||
|
try {
|
||||||
|
new ScheduleItem({
|
||||||
|
day: 0,
|
||||||
|
time: '07,00',
|
||||||
|
margin: 900,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentInvalidException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should throw an exception if the hour of the time is invalid', () => {
|
||||||
|
try {
|
||||||
|
new ScheduleItem({
|
||||||
|
day: 0,
|
||||||
|
time: '25:00',
|
||||||
|
margin: 900,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentInvalidException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should throw an exception if the minutes of the time are invalid', () => {
|
||||||
|
try {
|
||||||
|
new ScheduleItem({
|
||||||
|
day: 0,
|
||||||
|
time: '07:63',
|
||||||
|
margin: 900,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentInvalidException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,69 @@
|
||||||
|
import {
|
||||||
|
ArgumentInvalidException,
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
|
||||||
|
|
||||||
|
describe('Waypoint value object', () => {
|
||||||
|
it('should create a waypoint value object', () => {
|
||||||
|
const waypointVO = new Waypoint({
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
});
|
||||||
|
expect(waypointVO.position).toBe(0);
|
||||||
|
expect(waypointVO.lat).toBe(48.689445);
|
||||||
|
expect(waypointVO.lon).toBe(6.17651);
|
||||||
|
});
|
||||||
|
it('should throw an exception if position is invalid', () => {
|
||||||
|
try {
|
||||||
|
new Waypoint({
|
||||||
|
position: -1,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentInvalidException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should throw an exception if longitude is invalid', () => {
|
||||||
|
try {
|
||||||
|
new Waypoint({
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 186.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new Waypoint({
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: -186.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should throw an exception if latitude is invalid', () => {
|
||||||
|
try {
|
||||||
|
new Waypoint({
|
||||||
|
position: 0,
|
||||||
|
lat: 148.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
new Waypoint({
|
||||||
|
position: 0,
|
||||||
|
lat: -148.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,176 +0,0 @@
|
||||||
import { CreateAdRequest } from '../../../domain/dtos/create-ad.request';
|
|
||||||
import { CreateAdUseCase } from '../../../domain/usecases/create-ad.usecase';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AutomapperModule } from '@automapper/nestjs';
|
|
||||||
import { classes } from '@automapper/classes';
|
|
||||||
import { AdRepository } from '../../../adapters/secondaries/ad.repository';
|
|
||||||
import { CreateAdCommand } from '../../../commands/create-ad.command';
|
|
||||||
import { Ad } from '../../../domain/entities/ad';
|
|
||||||
import { AdProfile } from '../../../mappers/ad.profile';
|
|
||||||
import { Frequency } from '../../../domain/types/frequency.enum';
|
|
||||||
import { RouteKey } from '../../../domain/entities/geography';
|
|
||||||
import { DatabaseException } from '../../../../database/exceptions/database.exception';
|
|
||||||
import { Route } from '../../../../geography/domain/entities/route';
|
|
||||||
|
|
||||||
const mockAdRepository = {
|
|
||||||
createAd: jest.fn().mockImplementation((ad) => {
|
|
||||||
if (ad.uuid == '00000000-0000-0000-0000-000000000000')
|
|
||||||
throw new DatabaseException();
|
|
||||||
return new Ad();
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
const mockGeorouterCreator = {
|
|
||||||
create: jest.fn().mockImplementation(() => ({
|
|
||||||
route: jest.fn().mockImplementation(() => [
|
|
||||||
{
|
|
||||||
key: RouteKey.DRIVER,
|
|
||||||
route: <Route>{
|
|
||||||
points: [],
|
|
||||||
fwdAzimuth: 0,
|
|
||||||
backAzimuth: 180,
|
|
||||||
distance: 20000,
|
|
||||||
duration: 1800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: RouteKey.PASSENGER,
|
|
||||||
route: <Route>{
|
|
||||||
points: [],
|
|
||||||
fwdAzimuth: 0,
|
|
||||||
backAzimuth: 180,
|
|
||||||
distance: 20000,
|
|
||||||
duration: 1800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: RouteKey.COMMON,
|
|
||||||
route: <Route>{
|
|
||||||
points: [],
|
|
||||||
fwdAzimuth: 0,
|
|
||||||
backAzimuth: 180,
|
|
||||||
distance: 20000,
|
|
||||||
duration: 1800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
const mockParamsProvider = {
|
|
||||||
getParams: jest.fn().mockImplementation(() => ({
|
|
||||||
DEFAULT_TIMEZONE: 'Europe/Paris',
|
|
||||||
GEOROUTER_TYPE: 'graphhopper',
|
|
||||||
GEOROUTER_URL: 'localhost',
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
const mockTimezoneFinder = {
|
|
||||||
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
|
|
||||||
};
|
|
||||||
const mockDirectionEncoder = {
|
|
||||||
encode: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const createAdRequest: CreateAdRequest = {
|
|
||||||
uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1',
|
|
||||||
userUuid: 'dfd993f6-7889-4876-9570-5e1d7b6e3f42',
|
|
||||||
driver: true,
|
|
||||||
passenger: false,
|
|
||||||
frequency: Frequency.RECURRENT,
|
|
||||||
fromDate: new Date('2023-04-26'),
|
|
||||||
toDate: new Date('2024-04-25'),
|
|
||||||
monTime: '07:00',
|
|
||||||
tueTime: '07:00',
|
|
||||||
wedTime: '07:00',
|
|
||||||
thuTime: '07:00',
|
|
||||||
friTime: '07:00',
|
|
||||||
satTime: null,
|
|
||||||
sunTime: null,
|
|
||||||
monMargin: 900,
|
|
||||||
tueMargin: 900,
|
|
||||||
wedMargin: 900,
|
|
||||||
thuMargin: 900,
|
|
||||||
friMargin: 900,
|
|
||||||
satMargin: 900,
|
|
||||||
sunMargin: 900,
|
|
||||||
seatsDriver: 3,
|
|
||||||
seatsPassenger: 1,
|
|
||||||
strict: false,
|
|
||||||
waypoints: [
|
|
||||||
{ lon: 6, lat: 45 },
|
|
||||||
{ lon: 6.5, lat: 45.5 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const setUuid = async (uuid: string): Promise<void> => {
|
|
||||||
createAdRequest.uuid = uuid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setIsDriver = async (isDriver: boolean): Promise<void> => {
|
|
||||||
createAdRequest.driver = isDriver;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setIsPassenger = async (isPassenger: boolean): Promise<void> => {
|
|
||||||
createAdRequest.passenger = isPassenger;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('CreateAdUseCase', () => {
|
|
||||||
let createAdUseCase: CreateAdUseCase;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: AdRepository,
|
|
||||||
useValue: mockAdRepository,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'GeorouterCreator',
|
|
||||||
useValue: mockGeorouterCreator,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'ParamsProvider',
|
|
||||||
useValue: mockParamsProvider,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'TimezoneFinder',
|
|
||||||
useValue: mockTimezoneFinder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'DirectionEncoder',
|
|
||||||
useValue: mockDirectionEncoder,
|
|
||||||
},
|
|
||||||
AdProfile,
|
|
||||||
CreateAdUseCase,
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
createAdUseCase = module.get<CreateAdUseCase>(CreateAdUseCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(createAdUseCase).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('execute', () => {
|
|
||||||
it('should create an ad as driver', async () => {
|
|
||||||
const ad = await createAdUseCase.execute(
|
|
||||||
new CreateAdCommand(createAdRequest),
|
|
||||||
);
|
|
||||||
expect(ad).toBeInstanceOf(Ad);
|
|
||||||
});
|
|
||||||
it('should create an ad as passenger', async () => {
|
|
||||||
await setIsDriver(false);
|
|
||||||
await setIsPassenger(true);
|
|
||||||
const ad = await createAdUseCase.execute(
|
|
||||||
new CreateAdCommand(createAdRequest),
|
|
||||||
);
|
|
||||||
expect(ad).toBeInstanceOf(Ad);
|
|
||||||
});
|
|
||||||
it('should throw an exception if repository fails', async () => {
|
|
||||||
await setUuid('00000000-0000-0000-0000-000000000000');
|
|
||||||
await expect(
|
|
||||||
createAdUseCase.execute(new CreateAdCommand(createAdRequest)),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,138 +0,0 @@
|
||||||
import { Role } from '../../../domain/types/role.enum';
|
|
||||||
import { Geography } from '../../../domain/entities/geography';
|
|
||||||
import { Coordinate } from '../../../../geography/domain/entities/coordinate';
|
|
||||||
import { IGeorouter } from '../../../../geography/domain/interfaces/georouter.interface';
|
|
||||||
import { GeorouterSettings } from '../../../../geography/domain/types/georouter-settings.type';
|
|
||||||
import { Route } from '../../../../geography/domain/entities/route';
|
|
||||||
import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface';
|
|
||||||
|
|
||||||
const simpleCoordinates: Coordinate[] = [
|
|
||||||
{
|
|
||||||
lon: 6,
|
|
||||||
lat: 47,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 6.1,
|
|
||||||
lat: 47.1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const complexCoordinates: Coordinate[] = [
|
|
||||||
{
|
|
||||||
lon: 6,
|
|
||||||
lat: 47,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 6.1,
|
|
||||||
lat: 47.1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lon: 6.2,
|
|
||||||
lat: 47.2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockGeodesic: IGeodesic = {
|
|
||||||
inverse: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const driverRoute: Route = new Route(mockGeodesic);
|
|
||||||
driverRoute.distance = 25000;
|
|
||||||
|
|
||||||
const commonRoute: Route = new Route(mockGeodesic);
|
|
||||||
commonRoute.distance = 20000;
|
|
||||||
|
|
||||||
const mockGeorouter: IGeorouter = {
|
|
||||||
route: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce([
|
|
||||||
{
|
|
||||||
key: 'driver',
|
|
||||||
route: driverRoute,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.mockResolvedValueOnce([
|
|
||||||
{
|
|
||||||
key: 'passenger',
|
|
||||||
route: commonRoute,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.mockResolvedValueOnce([
|
|
||||||
{
|
|
||||||
key: 'common',
|
|
||||||
route: commonRoute,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.mockResolvedValueOnce([
|
|
||||||
{
|
|
||||||
key: 'driver',
|
|
||||||
route: driverRoute,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'passenger',
|
|
||||||
route: commonRoute,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
const georouterSettings: GeorouterSettings = {
|
|
||||||
withDistance: false,
|
|
||||||
withPoints: true,
|
|
||||||
withTime: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Geography entity', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(new Geography(simpleCoordinates)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a route as driver', async () => {
|
|
||||||
const geography = new Geography(complexCoordinates);
|
|
||||||
await geography.createRoutes(
|
|
||||||
[Role.DRIVER],
|
|
||||||
mockGeorouter,
|
|
||||||
georouterSettings,
|
|
||||||
);
|
|
||||||
expect(geography.driverRoute).toBeDefined();
|
|
||||||
expect(geography.passengerRoute).toBeUndefined();
|
|
||||||
expect(geography.driverRoute.distance).toBe(25000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a route as passenger', async () => {
|
|
||||||
const geography = new Geography(simpleCoordinates);
|
|
||||||
await geography.createRoutes(
|
|
||||||
[Role.PASSENGER],
|
|
||||||
mockGeorouter,
|
|
||||||
georouterSettings,
|
|
||||||
);
|
|
||||||
expect(geography.driverRoute).toBeUndefined();
|
|
||||||
expect(geography.passengerRoute).toBeDefined();
|
|
||||||
expect(geography.passengerRoute.distance).toBe(20000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create routes as driver and passenger with simple coordinates', async () => {
|
|
||||||
const geography = new Geography(simpleCoordinates);
|
|
||||||
await geography.createRoutes(
|
|
||||||
[Role.DRIVER, Role.PASSENGER],
|
|
||||||
mockGeorouter,
|
|
||||||
georouterSettings,
|
|
||||||
);
|
|
||||||
expect(geography.driverRoute).toBeDefined();
|
|
||||||
expect(geography.passengerRoute).toBeDefined();
|
|
||||||
expect(geography.driverRoute.distance).toBe(20000);
|
|
||||||
expect(geography.passengerRoute.distance).toBe(20000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create routes as driver and passenger with complex coordinates', async () => {
|
|
||||||
const geography = new Geography(complexCoordinates);
|
|
||||||
await geography.createRoutes(
|
|
||||||
[Role.DRIVER, Role.PASSENGER],
|
|
||||||
mockGeorouter,
|
|
||||||
georouterSettings,
|
|
||||||
);
|
|
||||||
expect(geography.driverRoute).toBeDefined();
|
|
||||||
expect(geography.passengerRoute).toBeDefined();
|
|
||||||
expect(geography.driverRoute.distance).toBe(25000);
|
|
||||||
expect(geography.passengerRoute.distance).toBe(20000);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { TimeConverter } from '../../../domain/entities/time-converter';
|
|
||||||
|
|
||||||
describe('TimeConverter', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(new TimeConverter()).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert a Europe/Paris datetime to utc datetime', () => {
|
|
||||||
expect(
|
|
||||||
TimeConverter.toUtcDatetime(
|
|
||||||
new Date('2023-05-01'),
|
|
||||||
'07:00',
|
|
||||||
'Europe/Paris',
|
|
||||||
).getUTCHours(),
|
|
||||||
).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when trying to convert a Europe/Paris datetime to utc datetime without a valid date', () => {
|
|
||||||
expect(
|
|
||||||
TimeConverter.toUtcDatetime(undefined, '07:00', 'Europe/Paris'),
|
|
||||||
).toBeUndefined();
|
|
||||||
expect(
|
|
||||||
TimeConverter.toUtcDatetime(
|
|
||||||
new Date('2023-13-01'),
|
|
||||||
'07:00',
|
|
||||||
'Europe/Paris',
|
|
||||||
),
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when trying to convert a Europe/Paris datetime to utc datetime without a valid time', () => {
|
|
||||||
expect(
|
|
||||||
TimeConverter.toUtcDatetime(
|
|
||||||
new Date('2023-05-01'),
|
|
||||||
undefined,
|
|
||||||
'Europe/Paris',
|
|
||||||
),
|
|
||||||
).toBeUndefined();
|
|
||||||
expect(
|
|
||||||
TimeConverter.toUtcDatetime(new Date('2023-05-01'), 'a', 'Europe/Paris'),
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when trying to convert a datetime to utc datetime without a valid timezone', () => {
|
|
||||||
expect(
|
|
||||||
TimeConverter.toUtcDatetime(
|
|
||||||
new Date('2023-12-01'),
|
|
||||||
'07:00',
|
|
||||||
'OlympusMons/Mars',
|
|
||||||
),
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {
|
||||||
|
AD_DIRECTION_ENCODER,
|
||||||
|
AD_ROUTE_PROVIDER,
|
||||||
|
} from '@modules/ad/ad.di-tokens';
|
||||||
|
import { AdMapper } from '@modules/ad/ad.mapper';
|
||||||
|
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||||
|
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
|
||||||
|
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
|
||||||
|
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||||
|
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const mockMessagePublisher = {
|
||||||
|
publish: jest.fn().mockImplementation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDirectionEncoder: DirectionEncoderPort = {
|
||||||
|
encode: jest.fn(),
|
||||||
|
decode: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRouteProvider: RouteProviderPort = {
|
||||||
|
getBasic: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Ad repository', () => {
|
||||||
|
let prismaService: PrismaService;
|
||||||
|
let adMapper: AdMapper;
|
||||||
|
let eventEmitter: EventEmitter2;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [EventEmitterModule.forRoot()],
|
||||||
|
providers: [
|
||||||
|
PrismaService,
|
||||||
|
AdMapper,
|
||||||
|
{
|
||||||
|
provide: AD_DIRECTION_ENCODER,
|
||||||
|
useValue: mockDirectionEncoder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AD_ROUTE_PROVIDER,
|
||||||
|
useValue: mockRouteProvider,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
prismaService = module.get<PrismaService>(PrismaService);
|
||||||
|
adMapper = module.get<AdMapper>(AdMapper);
|
||||||
|
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
|
||||||
|
});
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(
|
||||||
|
new AdRepository(
|
||||||
|
prismaService,
|
||||||
|
adMapper,
|
||||||
|
eventEmitter,
|
||||||
|
mockMessagePublisher,
|
||||||
|
),
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { Route } from '@modules/ad/core/application/types/route.type';
|
||||||
|
import { Role } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { RouteProvider } from '@modules/ad/infrastructure/route-provider';
|
||||||
|
import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const mockGetBasicRouteController: GetBasicRouteControllerPort = {
|
||||||
|
get: jest.fn().mockImplementation(() => ({
|
||||||
|
driverDistance: 23000,
|
||||||
|
driverDuration: 900,
|
||||||
|
passengerDistance: 23000,
|
||||||
|
passengerDuration: 900,
|
||||||
|
fwdAzimuth: 283,
|
||||||
|
backAzimuth: 93,
|
||||||
|
distanceAzimuth: 19840,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
lon: 6.1765103,
|
||||||
|
lat: 48.689446,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 2.3523,
|
||||||
|
lat: 48.8567,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Route provider', () => {
|
||||||
|
let routeProvider: RouteProvider;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
RouteProvider,
|
||||||
|
{
|
||||||
|
provide: AD_GET_BASIC_ROUTE_CONTROLLER,
|
||||||
|
useValue: mockGetBasicRouteController,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
routeProvider = module.get<RouteProvider>(RouteProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(routeProvider).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide a route', async () => {
|
||||||
|
const route: Route = await routeProvider.getBasic(
|
||||||
|
[Role.DRIVER],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.1765102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
lat: 48.8566,
|
||||||
|
lon: 2.3522,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(route.driverDistance).toBe(23000);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { AdCreatedMessageHandler } from '@modules/ad/interface/message-handlers/ad-created.message-handler';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const adCreatedMessage =
|
||||||
|
'{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driver":"true","passenger":"true","frequency":"PUNCTUAL","fromDate":"2023-08-18","toDate":"2023-08-18","schedule":[{"day":"5","time":"10:00","margin":"900"}],"seatsProposed":"3","seatsRequested":"1","strict":"false","waypoints":[{"position":"0","houseNumber":"5","street":"rue de la monnaie","locality":"Nancy","postalCode":"54000","country":"France","lon":"48.689445","lat":"6.17651"},{"position":"1","locality":"Paris","postalCode":"75000","country":"France","lon":"48.8566","lat":"2.3522"}]}';
|
||||||
|
|
||||||
|
const mockCommandBus = {
|
||||||
|
execute: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Ad Created Message Handler', () => {
|
||||||
|
let adCreatedMessageHandler: AdCreatedMessageHandler;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CommandBus,
|
||||||
|
useValue: mockCommandBus,
|
||||||
|
},
|
||||||
|
AdCreatedMessageHandler,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
adCreatedMessageHandler = module.get<AdCreatedMessageHandler>(
|
||||||
|
AdCreatedMessageHandler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(adCreatedMessageHandler).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an ad', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
await adCreatedMessageHandler.adCreated(adCreatedMessage);
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,259 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { DatabaseException } from '../../exceptions/database.exception';
|
|
||||||
import { ICollection } from '../../interfaces/collection.interface';
|
|
||||||
import { IRepository } from '../../interfaces/repository.interface';
|
|
||||||
import { PrismaService } from './prisma.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Child classes MUST redefined model property with appropriate model name
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export abstract class PrismaRepository<T> implements IRepository<T> {
|
|
||||||
protected model: string;
|
|
||||||
|
|
||||||
constructor(protected readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
findAll = async (
|
|
||||||
page = 1,
|
|
||||||
perPage = 10,
|
|
||||||
where?: any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<ICollection<T>> => {
|
|
||||||
const [data, total] = await this.prisma.$transaction([
|
|
||||||
this.prisma[this.model].findMany({
|
|
||||||
where,
|
|
||||||
include,
|
|
||||||
skip: (page - 1) * perPage,
|
|
||||||
take: perPage,
|
|
||||||
}),
|
|
||||||
this.prisma[this.model].count({
|
|
||||||
where,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
return Promise.resolve({
|
|
||||||
data,
|
|
||||||
total,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
findOneByUuid = async (uuid: string): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].findUnique({
|
|
||||||
where: { uuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findOne = async (where: any, include?: any): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].findFirst({
|
|
||||||
where: where,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO : using any is not good, but needed for nested entities
|
|
||||||
// TODO : Refactor for good clean architecture ?
|
|
||||||
async create(entity: Partial<T> | any, include?: any): Promise<T> {
|
|
||||||
try {
|
|
||||||
const res = await this.prisma[this.model].create({
|
|
||||||
data: entity,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update = async (uuid: string, entity: Partial<T>): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const updatedEntity = await this.prisma[this.model].update({
|
|
||||||
where: { uuid },
|
|
||||||
data: entity,
|
|
||||||
});
|
|
||||||
return updatedEntity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateWhere = async (
|
|
||||||
where: any,
|
|
||||||
entity: Partial<T> | any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const updatedEntity = await this.prisma[this.model].update({
|
|
||||||
where: where,
|
|
||||||
data: entity,
|
|
||||||
include: include,
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedEntity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
delete = async (uuid: string): Promise<T> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].delete({
|
|
||||||
where: { uuid },
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteMany = async (where: any): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const entity = await this.prisma[this.model].deleteMany({
|
|
||||||
where: where,
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findAllByQuery = async (
|
|
||||||
include: string[],
|
|
||||||
where: string[],
|
|
||||||
): Promise<ICollection<T>> => {
|
|
||||||
const query = `SELECT ${include.join(',')} FROM ${
|
|
||||||
this.model
|
|
||||||
} WHERE ${where.join(' AND ')}`;
|
|
||||||
const data: T[] = await this.prisma.$queryRawUnsafe(query);
|
|
||||||
return Promise.resolve({
|
|
||||||
data,
|
|
||||||
total: data.length,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
createWithFields = async (fields: object): Promise<number> => {
|
|
||||||
try {
|
|
||||||
const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join(
|
|
||||||
'","',
|
|
||||||
)}") VALUES (${Object.values(fields).join(',')})`;
|
|
||||||
return await this.prisma.$executeRawUnsafe(command);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateWithFields = async (uuid: string, entity: object): Promise<number> => {
|
|
||||||
entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
|
|
||||||
const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
|
|
||||||
try {
|
|
||||||
const command = `UPDATE ${this.model} SET ${values.join(
|
|
||||||
', ',
|
|
||||||
)} WHERE uuid = '${uuid}'`;
|
|
||||||
return await this.prisma.$executeRawUnsafe(command);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
healthCheck = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await this.prisma.$queryRaw`SELECT 1`;
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseException(
|
|
||||||
Prisma.PrismaClientKnownRequestError.name,
|
|
||||||
e.code,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new DatabaseException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { PrismaService } from './adapters/secondaries/prisma.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [PrismaService],
|
|
||||||
exports: [PrismaService],
|
|
||||||
})
|
|
||||||
export class DatabaseModule {}
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { PrismaRepository } from '../adapters/secondaries/prisma.repository.abstract';
|
|
||||||
|
|
||||||
export class DatabaseRepository<T> extends PrismaRepository<T> {}
|
|
|
@ -1,24 +0,0 @@
|
||||||
export class DatabaseException implements Error {
|
|
||||||
name: string;
|
|
||||||
message: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private _type: string = 'unknown',
|
|
||||||
private _code: string = '',
|
|
||||||
message?: string,
|
|
||||||
) {
|
|
||||||
this.name = 'DatabaseException';
|
|
||||||
this.message = message ?? 'An error occured with the database.';
|
|
||||||
if (this.message.includes('Unique constraint failed')) {
|
|
||||||
this.message = 'Already exists.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get type(): string {
|
|
||||||
return this._type;
|
|
||||||
}
|
|
||||||
|
|
||||||
get code(): string {
|
|
||||||
return this._code;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface ICollection<T> {
|
|
||||||
data: T[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { ICollection } from './collection.interface';
|
|
||||||
|
|
||||||
export interface IRepository<T> {
|
|
||||||
findAll(
|
|
||||||
page: number,
|
|
||||||
perPage: number,
|
|
||||||
params?: any,
|
|
||||||
include?: any,
|
|
||||||
): Promise<ICollection<T>>;
|
|
||||||
findOne(where: any, include?: any): Promise<T>;
|
|
||||||
findOneByUuid(uuid: string, include?: any): Promise<T>;
|
|
||||||
create(entity: Partial<T> | any, include?: any): Promise<T>;
|
|
||||||
update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
|
|
||||||
updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
|
|
||||||
delete(uuid: string): Promise<T>;
|
|
||||||
deleteMany(where: any): Promise<void>;
|
|
||||||
healthCheck(): Promise<boolean>;
|
|
||||||
}
|
|
|
@ -1,571 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PrismaService } from '../../adapters/secondaries/prisma.service';
|
|
||||||
import { PrismaRepository } from '../../adapters/secondaries/prisma.repository.abstract';
|
|
||||||
import { DatabaseException } from '../../exceptions/database.exception';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
class FakeEntity {
|
|
||||||
uuid?: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let entityId = 2;
|
|
||||||
const entityUuid = 'uuid-';
|
|
||||||
const entityName = 'name-';
|
|
||||||
|
|
||||||
const createRandomEntity = (): FakeEntity => {
|
|
||||||
const entity: FakeEntity = {
|
|
||||||
uuid: `${entityUuid}${entityId}`,
|
|
||||||
name: `${entityName}${entityId}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
entityId++;
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntityToCreate: FakeEntity = {
|
|
||||||
name: 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntityCreated: FakeEntity = {
|
|
||||||
...fakeEntityToCreate,
|
|
||||||
uuid: 'some-uuid',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fakeEntities: FakeEntity[] = [];
|
|
||||||
Array.from({ length: 10 }).forEach(() => {
|
|
||||||
fakeEntities.push(createRandomEntity());
|
|
||||||
});
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
class FakePrismaRepository extends PrismaRepository<FakeEntity> {
|
|
||||||
protected model = 'fake';
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakePrismaService extends PrismaService {
|
|
||||||
fake: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockPrismaService = {
|
|
||||||
$transaction: jest.fn().mockImplementation(async (data: any) => {
|
|
||||||
const entities = await data[0];
|
|
||||||
if (entities.length == 1) {
|
|
||||||
return Promise.resolve([[fakeEntityCreated], 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve([fakeEntities, fakeEntities.length]);
|
|
||||||
}),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
$queryRawUnsafe: jest.fn().mockImplementation((query?: string) => {
|
|
||||||
return Promise.resolve(fakeEntities);
|
|
||||||
}),
|
|
||||||
$executeRawUnsafe: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((fields: object) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
$queryRaw: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.mockImplementation(() => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
fake: {
|
|
||||||
create: jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(fakeEntityCreated)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
|
|
||||||
findMany: jest.fn().mockImplementation((params?: any) => {
|
|
||||||
if (params?.where?.limit == 1) {
|
|
||||||
return Promise.resolve([fakeEntityCreated]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(fakeEntities);
|
|
||||||
}),
|
|
||||||
count: jest.fn().mockResolvedValue(fakeEntities.length),
|
|
||||||
|
|
||||||
findUnique: jest.fn().mockImplementation(async (params?: any) => {
|
|
||||||
let entity;
|
|
||||||
|
|
||||||
if (params?.where?.uuid) {
|
|
||||||
entity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === params?.where?.uuid,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entity && params?.where?.uuid == 'unknown') {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
} else if (!entity) {
|
|
||||||
throw new Error('no entity');
|
|
||||||
}
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}),
|
|
||||||
|
|
||||||
findFirst: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
if (params?.where?.name) {
|
|
||||||
return Promise.resolve(
|
|
||||||
fakeEntities.find((entity) => entity.name === params?.where?.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Error('an unknown error');
|
|
||||||
}),
|
|
||||||
|
|
||||||
update: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementationOnce((params: any) => {
|
|
||||||
const entity = fakeEntities.find(
|
|
||||||
(entity) => entity.name === params.where.name,
|
|
||||||
);
|
|
||||||
Object.entries(params.data).map(([key, value]) => {
|
|
||||||
entity[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(entity);
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
const entity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === params.where.uuid,
|
|
||||||
);
|
|
||||||
Object.entries(params.data).map(([key, value]) => {
|
|
||||||
entity[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.resolve(entity);
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
fakeEntities.forEach((entity, index) => {
|
|
||||||
if (entity.uuid === params?.where?.uuid) {
|
|
||||||
found = true;
|
|
||||||
fakeEntities.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
deleteMany: jest
|
|
||||||
.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
.mockImplementationOnce((params?: any) => {
|
|
||||||
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
|
|
||||||
code: 'code',
|
|
||||||
clientVersion: 'version',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.mockImplementation((params: any) => {
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
fakeEntities.forEach((entity, index) => {
|
|
||||||
if (entity.uuid === params?.where?.uuid) {
|
|
||||||
found = true;
|
|
||||||
fakeEntities.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('PrismaRepository', () => {
|
|
||||||
let fakeRepository: FakePrismaRepository;
|
|
||||||
let prisma: FakePrismaService;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
FakePrismaRepository,
|
|
||||||
{
|
|
||||||
provide: PrismaService,
|
|
||||||
useValue: mockPrismaService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
fakeRepository = module.get<FakePrismaRepository>(FakePrismaRepository);
|
|
||||||
prisma = module.get<PrismaService>(PrismaService) as FakePrismaService;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(fakeRepository).toBeDefined();
|
|
||||||
expect(prisma).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAll', () => {
|
|
||||||
it('should return an array of entities', async () => {
|
|
||||||
jest.spyOn(prisma.fake, 'findMany');
|
|
||||||
jest.spyOn(prisma.fake, 'count');
|
|
||||||
jest.spyOn(prisma, '$transaction');
|
|
||||||
|
|
||||||
const entities = await fakeRepository.findAll();
|
|
||||||
expect(entities).toStrictEqual({
|
|
||||||
data: fakeEntities,
|
|
||||||
total: fakeEntities.length,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an array containing only one entity', async () => {
|
|
||||||
const entities = await fakeRepository.findAll(1, 10, { limit: 1 });
|
|
||||||
|
|
||||||
expect(prisma.fake.findMany).toHaveBeenCalledWith({
|
|
||||||
skip: 0,
|
|
||||||
take: 10,
|
|
||||||
where: { limit: 1 },
|
|
||||||
});
|
|
||||||
expect(entities).toEqual({
|
|
||||||
data: [fakeEntityCreated],
|
|
||||||
total: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
it('should create an entity', async () => {
|
|
||||||
jest.spyOn(prisma.fake, 'create');
|
|
||||||
|
|
||||||
const newEntity = await fakeRepository.create(fakeEntityToCreate);
|
|
||||||
expect(newEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.fake.create).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.create(fakeEntityToCreate),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.create(fakeEntityToCreate),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOneByUuid', () => {
|
|
||||||
it('should find an entity by uuid', async () => {
|
|
||||||
const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid);
|
|
||||||
expect(entity).toBe(fakeEntities[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneByUuid('unknown'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOneByUuid('wrong-uuid'),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findOne', () => {
|
|
||||||
it('should find one entity', async () => {
|
|
||||||
const entity = await fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(entity.name).toBe(fakeEntities[0].name);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for unknown error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.findOne({
|
|
||||||
name: fakeEntities[0].name,
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update an entity with name', async () => {
|
|
||||||
const newName = 'new-random-name';
|
|
||||||
|
|
||||||
await fakeRepository.updateWhere(
|
|
||||||
{ name: fakeEntities[0].name },
|
|
||||||
{
|
|
||||||
name: newName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(fakeEntities[0].name).toBe(newName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update an entity with uuid', async () => {
|
|
||||||
const newName = 'random-name';
|
|
||||||
|
|
||||||
await fakeRepository.update(fakeEntities[0].uuid, {
|
|
||||||
name: newName,
|
|
||||||
});
|
|
||||||
expect(fakeEntities[0].name).toBe(newName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.update('fake-uuid', { name: 'error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete an entity', async () => {
|
|
||||||
const savedUuid = fakeEntities[0].uuid;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const res = await fakeRepository.delete(savedUuid);
|
|
||||||
|
|
||||||
const deletedEntity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === savedUuid,
|
|
||||||
);
|
|
||||||
expect(deletedEntity).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteMany', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete entities based on their uuid', async () => {
|
|
||||||
const savedUuid = fakeEntities[0].uuid;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const res = await fakeRepository.deleteMany({ uuid: savedUuid });
|
|
||||||
|
|
||||||
const deletedEntity = fakeEntities.find(
|
|
||||||
(entity) => entity.uuid === savedUuid,
|
|
||||||
);
|
|
||||||
expect(deletedEntity).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw an exception if an entity doesn't exist", async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findAllByquery', () => {
|
|
||||||
it('should return an array of entities', async () => {
|
|
||||||
const entities = await fakeRepository.findAllByQuery(
|
|
||||||
['uuid', 'name'],
|
|
||||||
['name is not null'],
|
|
||||||
);
|
|
||||||
expect(entities).toStrictEqual({
|
|
||||||
data: fakeEntities,
|
|
||||||
total: fakeEntities.length,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createWithFields', () => {
|
|
||||||
it('should create an entity', async () => {
|
|
||||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
|
||||||
|
|
||||||
const newEntity = await fakeRepository.createWithFields({
|
|
||||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
name: 'my-name',
|
|
||||||
});
|
|
||||||
expect(newEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.createWithFields({
|
|
||||||
uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
name: 'my-name',
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.createWithFields({
|
|
||||||
name: 'my-name',
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateWithFields', () => {
|
|
||||||
it('should update an entity', async () => {
|
|
||||||
jest.spyOn(prisma, '$queryRawUnsafe');
|
|
||||||
|
|
||||||
const updatedEntity = await fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(updatedEntity).toBe(fakeEntityCreated);
|
|
||||||
expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw a DatabaseException if uuid is not found', async () => {
|
|
||||||
await expect(
|
|
||||||
fakeRepository.updateWithFields(
|
|
||||||
'804319b3-a09b-4491-9f82-7976bfce0aff',
|
|
||||||
{
|
|
||||||
name: 'my-name',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toBeInstanceOf(DatabaseException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('healthCheck', () => {
|
|
||||||
it('should throw a DatabaseException for client error', async () => {
|
|
||||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return a healthy result', async () => {
|
|
||||||
const res = await fakeRepository.healthCheck();
|
|
||||||
expect(res).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an exception if database is not available', async () => {
|
|
||||||
await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
|
|
||||||
DatabaseException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { IFindTimezone } from '../../domain/interfaces/timezone-finder.interface';
|
|
||||||
import { find } from 'geo-tz';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GeoTimezoneFinder implements IFindTimezone {
|
|
||||||
timezones = (lon: number, lat: number): string[] => find(lat, lon);
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface';
|
|
||||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
|
|
||||||
import { GraphhopperGeorouter } from './graphhopper-georouter';
|
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { Geodesic } from './geodesic';
|
|
||||||
import { GeographyException } from '../../exceptions/geography.exception';
|
|
||||||
import { ExceptionCode } from '../../..//utils/exception-code.enum';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GeorouterCreator implements ICreateGeorouter {
|
|
||||||
constructor(
|
|
||||||
private readonly httpService: HttpService,
|
|
||||||
private readonly geodesic: Geodesic,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
create = (type: string, url: string): IGeorouter => {
|
|
||||||
switch (type) {
|
|
||||||
case 'graphhopper':
|
|
||||||
return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
|
|
||||||
default:
|
|
||||||
throw new GeographyException(
|
|
||||||
ExceptionCode.INVALID_ARGUMENT,
|
|
||||||
'Unknown geocoder',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { Coordinate } from '../../domain/entities/coordinate';
|
|
||||||
import { IEncodeDirection } from '../../domain/interfaces/direction-encoder.interface';
|
|
||||||
|
|
||||||
export class PostgresDirectionEncoder implements IEncodeDirection {
|
|
||||||
encode = (coordinates: Coordinate[]): string =>
|
|
||||||
[
|
|
||||||
"'LINESTRING(",
|
|
||||||
coordinates.map((point) => [point.lon, point.lat].join(' ')).join(),
|
|
||||||
")'",
|
|
||||||
].join('');
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { DefaultParams } from '../types/default-params.type';
|
import { DefaultParams } from '../types/default-params.type';
|
||||||
|
|
||||||
export interface IProvideParams {
|
export interface DefaultParamsProviderPort {
|
||||||
getParams(): DefaultParams;
|
getParams(): DefaultParams;
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Coordinates } from '../../domain/route.types';
|
||||||
|
|
||||||
|
export interface DirectionEncoderPort {
|
||||||
|
encode(coordinates: Coordinates[]): string;
|
||||||
|
decode(direction: string): Coordinates[];
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export interface IGeodesic {
|
export interface GeodesicPort {
|
||||||
inverse(
|
inverse(
|
||||||
lon1: number,
|
lon1: number,
|
||||||
lat1: number,
|
lat1: number,
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Path, Route } from '../../domain/route.types';
|
||||||
|
import { GeorouterSettings } from '../types/georouter-settings.type';
|
||||||
|
|
||||||
|
export interface GeorouterPort {
|
||||||
|
routes(paths: Path[], settings: GeorouterSettings): Promise<Route[]>;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { GetRouteRequestDto } from '@modules/geography/interface/controllers/dtos/get-route.request.dto';
|
||||||
|
import { RouteResponseDto } from '@modules/geography/interface/dtos/route.response.dto';
|
||||||
|
|
||||||
|
export interface GetBasicRouteControllerPort {
|
||||||
|
get(data: GetRouteRequestDto): Promise<RouteResponseDto>;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
|
import { GetRouteQuery } from './get-route.query';
|
||||||
|
import { RouteEntity } from '@modules/geography/core/domain/route.entity';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { GEOROUTER } from '@modules/geography/geography.di-tokens';
|
||||||
|
import { GeorouterPort } from '../../ports/georouter.port';
|
||||||
|
|
||||||
|
@QueryHandler(GetRouteQuery)
|
||||||
|
export class GetRouteQueryHandler implements IQueryHandler {
|
||||||
|
constructor(@Inject(GEOROUTER) private readonly georouter: GeorouterPort) {}
|
||||||
|
|
||||||
|
execute = async (query: GetRouteQuery): Promise<RouteEntity> =>
|
||||||
|
await RouteEntity.create({
|
||||||
|
roles: query.roles,
|
||||||
|
waypoints: query.waypoints,
|
||||||
|
georouter: this.georouter,
|
||||||
|
georouterSettings: query.georouterSettings,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { QueryBase } from '@mobicoop/ddd-library';
|
||||||
|
import { Role, Waypoint } from '@modules/geography/core/domain/route.types';
|
||||||
|
import { GeorouterSettings } from '../../types/georouter-settings.type';
|
||||||
|
|
||||||
|
export class GetRouteQuery extends QueryBase {
|
||||||
|
readonly roles: Role[];
|
||||||
|
readonly waypoints: Waypoint[];
|
||||||
|
readonly georouterSettings: GeorouterSettings;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
roles: Role[],
|
||||||
|
waypoints: Waypoint[],
|
||||||
|
georouterSettings: GeorouterSettings,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.roles = roles;
|
||||||
|
this.waypoints = waypoints;
|
||||||
|
this.georouterSettings = georouterSettings;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
export type DefaultParams = {
|
export type DefaultParams = {
|
||||||
DEFAULT_TIMEZONE: string;
|
|
||||||
GEOROUTER_TYPE: string;
|
GEOROUTER_TYPE: string;
|
||||||
GEOROUTER_URL: string;
|
GEOROUTER_URL: string;
|
||||||
};
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type GeorouterSettings = {
|
||||||
|
points: boolean;
|
||||||
|
detailedDuration: boolean;
|
||||||
|
detailedDistance: boolean;
|
||||||
|
};
|
|
@ -0,0 +1,162 @@
|
||||||
|
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import {
|
||||||
|
CreateRouteProps,
|
||||||
|
Path,
|
||||||
|
Role,
|
||||||
|
RouteProps,
|
||||||
|
PathType,
|
||||||
|
Route,
|
||||||
|
} from './route.types';
|
||||||
|
import { WaypointProps } from './value-objects/waypoint.value-object';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { RouteNotFoundException } from './route.errors';
|
||||||
|
|
||||||
|
export class RouteEntity extends AggregateRoot<RouteProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
|
||||||
|
let routes: Route[];
|
||||||
|
try {
|
||||||
|
routes = await create.georouter.routes(
|
||||||
|
this.getPaths(create.roles, create.waypoints),
|
||||||
|
create.georouterSettings,
|
||||||
|
);
|
||||||
|
if (!routes || routes.length == 0) throw new RouteNotFoundException();
|
||||||
|
} catch (e: any) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
let driverRoute: Route;
|
||||||
|
let passengerRoute: Route;
|
||||||
|
if (routes.some((route: Route) => route.type == PathType.GENERIC)) {
|
||||||
|
driverRoute = passengerRoute = routes.find(
|
||||||
|
(route: Route) => route.type == PathType.GENERIC,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
driverRoute = routes.some((route: Route) => route.type == PathType.DRIVER)
|
||||||
|
? routes.find((route: Route) => route.type == PathType.DRIVER)
|
||||||
|
: undefined;
|
||||||
|
passengerRoute = routes.some(
|
||||||
|
(route: Route) => route.type == PathType.PASSENGER,
|
||||||
|
)
|
||||||
|
? routes.find((route: Route) => route.type == PathType.PASSENGER)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
const routeProps: RouteProps = {
|
||||||
|
driverDistance: driverRoute?.distance,
|
||||||
|
driverDuration: driverRoute?.duration,
|
||||||
|
passengerDistance: passengerRoute?.distance,
|
||||||
|
passengerDuration: passengerRoute?.duration,
|
||||||
|
fwdAzimuth: driverRoute
|
||||||
|
? driverRoute.fwdAzimuth
|
||||||
|
: passengerRoute.fwdAzimuth,
|
||||||
|
backAzimuth: driverRoute
|
||||||
|
? driverRoute.backAzimuth
|
||||||
|
: passengerRoute.backAzimuth,
|
||||||
|
distanceAzimuth: driverRoute
|
||||||
|
? driverRoute.distanceAzimuth
|
||||||
|
: passengerRoute.distanceAzimuth,
|
||||||
|
waypoints: create.waypoints,
|
||||||
|
points: driverRoute ? driverRoute.points : passengerRoute.points,
|
||||||
|
};
|
||||||
|
return new RouteEntity({
|
||||||
|
id: v4(),
|
||||||
|
props: routeProps,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getPaths = (
|
||||||
|
roles: Role[],
|
||||||
|
waypoints: WaypointProps[],
|
||||||
|
): Path[] => {
|
||||||
|
const paths: Path[] = [];
|
||||||
|
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
|
||||||
|
if (waypoints.length == 2) {
|
||||||
|
// 2 points => same route for driver and passenger
|
||||||
|
paths.push(this.createGenericPath(waypoints));
|
||||||
|
} else {
|
||||||
|
paths.push(
|
||||||
|
this.createDriverPath(waypoints),
|
||||||
|
this.createPassengerPath(waypoints),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (roles.includes(Role.DRIVER)) {
|
||||||
|
paths.push(this.createDriverPath(waypoints));
|
||||||
|
} else if (roles.includes(Role.PASSENGER)) {
|
||||||
|
paths.push(this.createPassengerPath(waypoints));
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
private static createGenericPath = (waypoints: WaypointProps[]): Path =>
|
||||||
|
this.createPath(waypoints, PathType.GENERIC);
|
||||||
|
|
||||||
|
private static createDriverPath = (waypoints: WaypointProps[]): Path =>
|
||||||
|
this.createPath(waypoints, PathType.DRIVER);
|
||||||
|
|
||||||
|
private static createPassengerPath = (waypoints: WaypointProps[]): Path =>
|
||||||
|
this.createPath(
|
||||||
|
[waypoints[0], waypoints[waypoints.length - 1]],
|
||||||
|
PathType.PASSENGER,
|
||||||
|
);
|
||||||
|
|
||||||
|
private static createPath = (
|
||||||
|
points: WaypointProps[],
|
||||||
|
type: PathType,
|
||||||
|
): Path => ({
|
||||||
|
type,
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// import { IGeodesic } from '../interfaces/geodesic.interface';
|
||||||
|
// import { Point } from '../types/point.type';
|
||||||
|
// import { SpacetimePoint } from './spacetime-point';
|
||||||
|
|
||||||
|
// export class Route {
|
||||||
|
// distance: number;
|
||||||
|
// duration: number;
|
||||||
|
// fwdAzimuth: number;
|
||||||
|
// backAzimuth: number;
|
||||||
|
// distanceAzimuth: number;
|
||||||
|
// points: Point[];
|
||||||
|
// spacetimePoints: SpacetimePoint[];
|
||||||
|
// private geodesic: IGeodesic;
|
||||||
|
|
||||||
|
// constructor(geodesic: IGeodesic) {
|
||||||
|
// this.distance = undefined;
|
||||||
|
// this.duration = undefined;
|
||||||
|
// this.fwdAzimuth = undefined;
|
||||||
|
// this.backAzimuth = undefined;
|
||||||
|
// this.distanceAzimuth = undefined;
|
||||||
|
// this.points = [];
|
||||||
|
// this.spacetimePoints = [];
|
||||||
|
// this.geodesic = geodesic;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setPoints = (points: Point[]): void => {
|
||||||
|
// this.points = points;
|
||||||
|
// this.setAzimuth(points);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// setSpacetimePoints = (spacetimePoints: SpacetimePoint[]): void => {
|
||||||
|
// this.spacetimePoints = spacetimePoints;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// protected setAzimuth = (points: Point[]): void => {
|
||||||
|
// const inverse = this.geodesic.inverse(
|
||||||
|
// points[0].lon,
|
||||||
|
// points[0].lat,
|
||||||
|
// points[points.length - 1].lon,
|
||||||
|
// points[points.length - 1].lat,
|
||||||
|
// );
|
||||||
|
// this.fwdAzimuth =
|
||||||
|
// inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth);
|
||||||
|
// this.backAzimuth =
|
||||||
|
// this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180;
|
||||||
|
// this.distanceAzimuth = inverse.distance;
|
||||||
|
// };
|
||||||
|
// }
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class RouteNotFoundException extends ExceptionBase {
|
||||||
|
static readonly message = 'Route not found';
|
||||||
|
|
||||||
|
public readonly code = 'ROUTE.NOT_FOUND';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(RouteNotFoundException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GeorouterUnavailableException extends ExceptionBase {
|
||||||
|
static readonly message = 'Georouter unavailable';
|
||||||
|
|
||||||
|
public readonly code = 'GEOROUTER.UNAVAILABLE';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(GeorouterUnavailableException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { GeorouterPort } from '../application/ports/georouter.port';
|
||||||
|
import { GeorouterSettings } from '../application/types/georouter-settings.type';
|
||||||
|
import { CoordinatesProps } from './value-objects/coordinates.value-object';
|
||||||
|
import { SpacetimePointProps } from './value-objects/spacetime-point.value-object';
|
||||||
|
import { WaypointProps } from './value-objects/waypoint.value-object';
|
||||||
|
|
||||||
|
// All properties that a Route has
|
||||||
|
export interface RouteProps {
|
||||||
|
driverDistance?: number;
|
||||||
|
driverDuration?: number;
|
||||||
|
passengerDistance?: number;
|
||||||
|
passengerDuration?: number;
|
||||||
|
fwdAzimuth: number;
|
||||||
|
backAzimuth: number;
|
||||||
|
distanceAzimuth: number;
|
||||||
|
waypoints: WaypointProps[];
|
||||||
|
points: SpacetimePointProps[] | CoordinatesProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties that are needed for a Route creation
|
||||||
|
export interface CreateRouteProps {
|
||||||
|
roles: Role[];
|
||||||
|
waypoints: WaypointProps[];
|
||||||
|
georouter: GeorouterPort;
|
||||||
|
georouterSettings: GeorouterSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
type: PathType;
|
||||||
|
distance: number;
|
||||||
|
duration: number;
|
||||||
|
fwdAzimuth: number;
|
||||||
|
backAzimuth: number;
|
||||||
|
distanceAzimuth: number;
|
||||||
|
points: Coordinates[];
|
||||||
|
spacetimeWaypoints: SpacetimePoint[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Path = {
|
||||||
|
type: PathType;
|
||||||
|
points: Coordinates[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Coordinates = {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Waypoint = Coordinates & {
|
||||||
|
position: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpacetimePoint = Coordinates & {
|
||||||
|
duration: number;
|
||||||
|
distance: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum Role {
|
||||||
|
DRIVER = 'DRIVER',
|
||||||
|
PASSENGER = 'PASSENGER',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PathType {
|
||||||
|
GENERIC = 'generic',
|
||||||
|
DRIVER = 'driver',
|
||||||
|
PASSENGER = 'passenger',
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
ValueObject,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface CoordinatesProps {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Coordinates extends ValueObject<CoordinatesProps> {
|
||||||
|
get lon(): number {
|
||||||
|
return this.props.lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat(): number {
|
||||||
|
return this.props.lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate(props: CoordinatesProps): void {
|
||||||
|
if (props.lon > 180 || props.lon < -180)
|
||||||
|
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
|
||||||
|
if (props.lat > 90 || props.lat < -90)
|
||||||
|
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {
|
||||||
|
ArgumentInvalidException,
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
ValueObject,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface SpacetimePointProps {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
duration: number;
|
||||||
|
distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpacetimePoint extends ValueObject<SpacetimePointProps> {
|
||||||
|
get lon(): number {
|
||||||
|
return this.props.lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat(): number {
|
||||||
|
return this.props.lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
get duration(): number {
|
||||||
|
return this.props.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
get distance(): number {
|
||||||
|
return this.props.distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate(props: SpacetimePointProps): void {
|
||||||
|
if (props.duration < 0)
|
||||||
|
throw new ArgumentInvalidException(
|
||||||
|
'duration must be greater than or equal to 0',
|
||||||
|
);
|
||||||
|
if (props.distance < 0)
|
||||||
|
throw new ArgumentInvalidException(
|
||||||
|
'distance must be greater than or equal to 0',
|
||||||
|
);
|
||||||
|
if (props.lon > 180 || props.lon < -180)
|
||||||
|
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
|
||||||
|
if (props.lat > 90 || props.lat < -90)
|
||||||
|
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import {
|
||||||
|
ArgumentInvalidException,
|
||||||
|
ArgumentOutOfRangeException,
|
||||||
|
ValueObject,
|
||||||
|
} from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface WaypointProps {
|
||||||
|
position: number;
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Waypoint extends ValueObject<WaypointProps> {
|
||||||
|
get position(): number {
|
||||||
|
return this.props.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lon(): number {
|
||||||
|
return this.props.lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lat(): number {
|
||||||
|
return this.props.lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate(props: WaypointProps): void {
|
||||||
|
if (props.position < 0)
|
||||||
|
throw new ArgumentInvalidException(
|
||||||
|
'position must be greater than or equal to 0',
|
||||||
|
);
|
||||||
|
if (props.lon > 180 || props.lon < -180)
|
||||||
|
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
|
||||||
|
if (props.lat > 90 || props.lat < -90)
|
||||||
|
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
import { AutoMap } from '@automapper/classes';
|
|
||||||
import { IsLatitude, IsLongitude, IsNumber } from 'class-validator';
|
|
||||||
|
|
||||||
export class Coordinate {
|
|
||||||
constructor(lon: number, lat: number) {
|
|
||||||
this.lon = lon;
|
|
||||||
this.lat = lat;
|
|
||||||
}
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@IsLongitude()
|
|
||||||
@AutoMap()
|
|
||||||
lon: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@IsLatitude()
|
|
||||||
@AutoMap()
|
|
||||||
lat: number;
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { IGeodesic } from '../interfaces/geodesic.interface';
|
|
||||||
import { Point } from '../types/point.type';
|
|
||||||
import { SpacetimePoint } from './spacetime-point';
|
|
||||||
|
|
||||||
export class Route {
|
|
||||||
distance: number;
|
|
||||||
duration: number;
|
|
||||||
fwdAzimuth: number;
|
|
||||||
backAzimuth: number;
|
|
||||||
distanceAzimuth: number;
|
|
||||||
points: Point[];
|
|
||||||
spacetimePoints: SpacetimePoint[];
|
|
||||||
private geodesic: IGeodesic;
|
|
||||||
|
|
||||||
constructor(geodesic: IGeodesic) {
|
|
||||||
this.distance = undefined;
|
|
||||||
this.duration = undefined;
|
|
||||||
this.fwdAzimuth = undefined;
|
|
||||||
this.backAzimuth = undefined;
|
|
||||||
this.distanceAzimuth = undefined;
|
|
||||||
this.points = [];
|
|
||||||
this.spacetimePoints = [];
|
|
||||||
this.geodesic = geodesic;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPoints = (points: Point[]): void => {
|
|
||||||
this.points = points;
|
|
||||||
this.setAzimuth(points);
|
|
||||||
};
|
|
||||||
|
|
||||||
setSpacetimePoints = (spacetimePoints: SpacetimePoint[]): void => {
|
|
||||||
this.spacetimePoints = spacetimePoints;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected setAzimuth = (points: Point[]): void => {
|
|
||||||
const inverse = this.geodesic.inverse(
|
|
||||||
points[0].lon,
|
|
||||||
points[0].lat,
|
|
||||||
points[points.length - 1].lon,
|
|
||||||
points[points.length - 1].lat,
|
|
||||||
);
|
|
||||||
this.fwdAzimuth =
|
|
||||||
inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth);
|
|
||||||
this.backAzimuth =
|
|
||||||
this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180;
|
|
||||||
this.distanceAzimuth = inverse.distance;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { Coordinate } from './coordinate';
|
|
||||||
|
|
||||||
export class SpacetimePoint {
|
|
||||||
coordinate: Coordinate;
|
|
||||||
duration: number;
|
|
||||||
distance: number;
|
|
||||||
|
|
||||||
constructor(coordinate: Coordinate, duration: number, distance: number) {
|
|
||||||
this.coordinate = coordinate;
|
|
||||||
this.duration = duration;
|
|
||||||
this.distance = distance;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { Coordinate } from '../entities/coordinate';
|
|
||||||
|
|
||||||
export interface IEncodeDirection {
|
|
||||||
encode(coordinates: Coordinate[]): string;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { IGeorouter } from './georouter.interface';
|
|
||||||
|
|
||||||
export interface ICreateGeorouter {
|
|
||||||
create(type: string, url: string): IGeorouter;
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { GeorouterSettings } from '../types/georouter-settings.type';
|
|
||||||
import { NamedRoute } from '../types/named-route';
|
|
||||||
import { Path } from '../types/path.type';
|
|
||||||
|
|
||||||
export interface IGeorouter {
|
|
||||||
route(paths: Path[], settings: GeorouterSettings): Promise<NamedRoute[]>;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export interface IFindTimezone {
|
|
||||||
timezones(lon: number, lat: number): string[];
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
export type GeorouterSettings = {
|
|
||||||
withPoints: boolean;
|
|
||||||
withTime: boolean;
|
|
||||||
withDistance: boolean;
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { Route } from '../entities/route';
|
|
||||||
|
|
||||||
export type NamedRoute = {
|
|
||||||
key: string;
|
|
||||||
route: Route;
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { Point } from '../../../geography/domain/types/point.type';
|
|
||||||
|
|
||||||
export type Path = {
|
|
||||||
key: string;
|
|
||||||
points: Point[];
|
|
||||||
};
|
|
|
@ -1,7 +0,0 @@
|
||||||
export enum PointType {
|
|
||||||
HOUSE_NUMBER = 'HOUSE_NUMBER',
|
|
||||||
STREET_ADDRESS = 'STREET_ADDRESS',
|
|
||||||
LOCALITY = 'LOCALITY',
|
|
||||||
VENUE = 'VENUE',
|
|
||||||
OTHER = 'OTHER',
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { PointType } from './point-type.enum';
|
|
||||||
import { Coordinate } from '../entities/coordinate';
|
|
||||||
|
|
||||||
export type Point = Coordinate & {
|
|
||||||
type?: PointType;
|
|
||||||
};
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { IFindTimezone } from '../interfaces/timezone-finder.interface';
|
|
||||||
|
|
||||||
export type Timezoner = {
|
|
||||||
timezone: string;
|
|
||||||
finder: IFindTimezone;
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue