diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..86b2c7c --- /dev/null +++ b/.env.test @@ -0,0 +1,7 @@ +# SERVICE +SERVICE_URL=0.0.0.0 +SERVICE_PORT=5005 +SERVICE_CONFIGURATION_DOMAIN=MATCHER + +# PRISMA +DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=matcher" diff --git a/package-lock.json b/package-lock.json index b95a8ba..e565461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,8 @@ "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "timezonecomplete": "^5.12.4" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -8576,6 +8577,14 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/timezonecomplete": { + "version": "5.12.4", + "resolved": "https://registry.npmjs.org/timezonecomplete/-/timezonecomplete-5.12.4.tgz", + "integrity": "sha512-K+ocagBAl5wu9Ifh5oHKhRRLb0wP7j0VjAzjboZsT6bnVmtJNRe3Wnk2IPp0C4Uc8HpLly3gbfUrTlJ3M7vCPA==", + "dependencies": { + "tzdata": "^1.0.25" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -8867,6 +8876,11 @@ "node": ">=4.2.0" } }, + "node_modules/tzdata": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/tzdata/-/tzdata-1.0.38.tgz", + "integrity": "sha512-KIgVvZTLt+DWzr3MOENNLCLdsNB+usedRYYHCVfVbA7TDewj8mfjlWmj3Mv6FfdrvfeE6Oprt+qE47YiL90duQ==" + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", diff --git a/package.json b/package.json index 43e918b..35861ee 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "got": "^11.8.6", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "timezonecomplete": "^5.12.4" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/prisma/migrations/20230406093419_init/migration.sql b/prisma/migrations/20230406093419_init/migration.sql deleted file mode 100644 index 836b706..0000000 --- a/prisma/migrations/20230406093419_init/migration.sql +++ /dev/null @@ -1,65 +0,0 @@ --- CreateExtension -CREATE EXTENSION IF NOT EXISTS "postgis"; - --- Required to use postgis extension : --- set the search_path to both public and territory (where is postgis) AND the current schema -SET search_path TO matcher, territory, public; - --- CreateTable -CREATE TABLE "ad" ( - "uuid" UUID NOT NULL, - "driver" BOOLEAN NOT NULL, - "passenger" BOOLEAN NOT NULL, - "frequency" INTEGER NOT NULL, - "from_date" DATE NOT NULL, - "to_date" DATE NOT NULL, - "mon_time" TIMESTAMPTZ NOT NULL, - "tue_time" TIMESTAMPTZ NOT NULL, - "wed_time" TIMESTAMPTZ NOT NULL, - "thu_time" TIMESTAMPTZ NOT NULL, - "fri_time" TIMESTAMPTZ NOT NULL, - "sat_time" TIMESTAMPTZ NOT NULL, - "sun_time" TIMESTAMPTZ NOT NULL, - "mon_margin" INTEGER NOT NULL, - "tue_margin" INTEGER NOT NULL, - "wed_margin" INTEGER NOT NULL, - "thu_margin" INTEGER NOT NULL, - "fri_margin" INTEGER NOT NULL, - "sat_margin" INTEGER NOT NULL, - "sun_margin" INTEGER NOT NULL, - "driver_duration" INTEGER NOT NULL, - "driver_distance" INTEGER NOT NULL, - "passenger_duration" INTEGER NOT NULL, - "passenger_distance" INTEGER NOT NULL, - "origin_type" SMALLINT NOT NULL, - "destination_type" SMALLINT NOT NULL, - "waypoints" geography(LINESTRING) NOT NULL, - "direction" geography(LINESTRING) NOT NULL, - "fwd_azimuth" INTEGER NOT NULL, - "back_azimuth" INTEGER NOT NULL, - "seats_driver" SMALLINT NOT NULL, - "seats_passenger" SMALLINT NOT NULL, - "seats_used" SMALLINT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid") -); - --- CreateIndex -CREATE INDEX "ad_driver_idx" ON "ad"("driver"); - --- CreateIndex -CREATE INDEX "ad_passenger_idx" ON "ad"("passenger"); - --- CreateIndex -CREATE INDEX "ad_from_date_idx" ON "ad"("from_date"); - --- CreateIndex -CREATE INDEX "ad_to_date_idx" ON "ad"("to_date"); - --- CreateIndex -CREATE INDEX "ad_fwd_azimuth_idx" ON "ad"("fwd_azimuth"); - --- CreateIndex -CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction"); diff --git a/prisma/migrations/20230512130750_init/migration.sql b/prisma/migrations/20230512130750_init/migration.sql new file mode 100644 index 0000000..f925dcd --- /dev/null +++ b/prisma/migrations/20230512130750_init/migration.sql @@ -0,0 +1,68 @@ +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "postgis"; + +-- Required to use postgis extension : +-- set the search_path to both public (where is postgis) AND the current schema +SET search_path TO matcher, public; + +-- CreateEnum +CREATE TYPE "Frequency" AS ENUM ('PUNCTUAL', 'RECURRENT'); + +-- CreateTable +CREATE TABLE "ad" ( + "uuid" UUID NOT NULL, + "userUuid" UUID NOT NULL, + "driver" BOOLEAN NOT NULL, + "passenger" BOOLEAN NOT NULL, + "frequency" "Frequency" NOT NULL, + "fromDate" DATE NOT NULL, + "toDate" DATE NOT NULL, + "monTime" TIMESTAMPTZ, + "tueTime" TIMESTAMPTZ, + "wedTime" TIMESTAMPTZ, + "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, + "driverDistance" INTEGER, + "passengerDuration" INTEGER, + "passengerDistance" INTEGER, + "waypoints" geography(LINESTRING), + "direction" geography(LINESTRING), + "fwdAzimuth" 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, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid") +); + +-- CreateIndex +CREATE INDEX "ad_driver_idx" ON "ad"("driver"); + +-- CreateIndex +CREATE INDEX "ad_passenger_idx" ON "ad"("passenger"); + +-- CreateIndex +CREATE INDEX "ad_fromDate_idx" ON "ad"("fromDate"); + +-- CreateIndex +CREATE INDEX "ad_toDate_idx" ON "ad"("toDate"); + +-- CreateIndex +CREATE INDEX "ad_fwdAzimuth_idx" ON "ad"("fwdAzimuth"); + +-- CreateIndex +CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f9b52df..0c3717f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,6 +3,7 @@ generator client { provider = "prisma-client-js" + binaryTargets = ["linux-musl", "debian-openssl-3.0.x"] previewFeatures = ["postgresqlExtensions"] } @@ -13,47 +14,52 @@ datasource db { } model Ad { - uuid String @id @default(uuid()) @db.Uuid - driver Boolean - passenger Boolean - frequency Int - from_date DateTime @db.Date - to_date DateTime @db.Date - mon_time DateTime @db.Timestamptz() - tue_time DateTime @db.Timestamptz() - wed_time DateTime @db.Timestamptz() - thu_time DateTime @db.Timestamptz() - fri_time DateTime @db.Timestamptz() - sat_time DateTime @db.Timestamptz() - sun_time DateTime @db.Timestamptz() - mon_margin Int - tue_margin Int - wed_margin Int - thu_margin Int - fri_margin Int - sat_margin Int - sun_margin Int - driver_duration Int - driver_distance Int - passenger_duration Int - passenger_distance Int - origin_type Int @db.SmallInt - destination_type Int @db.SmallInt - waypoints Unsupported("geography(LINESTRING)") - direction Unsupported("geography(LINESTRING)") - fwd_azimuth Int - back_azimuth Int - seats_driver Int @db.SmallInt - seats_passenger Int @db.SmallInt - seats_used Int @db.SmallInt - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt + uuid String @id @db.Uuid + userUuid String @db.Uuid + driver Boolean + passenger Boolean + frequency Frequency + fromDate DateTime @db.Date + toDate DateTime @db.Date + monTime DateTime? @db.Timestamptz() + tueTime DateTime? @db.Timestamptz() + wedTime DateTime? @db.Timestamptz() + thuTime DateTime? @db.Timestamptz() + 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? + driverDistance Int? + passengerDuration Int? + passengerDistance Int? + waypoints Unsupported("geography(LINESTRING)")? + direction Unsupported("geography(LINESTRING)")? + fwdAzimuth Int + backAzimuth Int + seatsDriver Int @db.SmallInt + seatsPassenger Int @db.SmallInt + seatsUsed Int @db.SmallInt + strict Boolean + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt @@index([driver]) @@index([passenger]) - @@index([from_date]) - @@index([to_date]) - @@index([fwd_azimuth]) + @@index([fromDate]) + @@index([toDate]) + @@index([fwdAzimuth]) @@index([direction], name: "direction_idx", type: Gist) @@map("ad") } + +enum Frequency { + PUNCTUAL + RECURRENT +} diff --git a/src/app.module.ts b/src/app.module.ts index 69bca0e..30da99c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config'; import { ConfigurationModule } from './modules/configuration/configuration.module'; import { HealthModule } from './modules/health/health.module'; import { MatcherModule } from './modules/matcher/matcher.module'; +import { AdModule } from './modules/ad/ad.module'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { MatcherModule } from './modules/matcher/matcher.module'; ConfigurationModule, HealthModule, MatcherModule, + AdModule, ], controllers: [], providers: [], diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts new file mode 100644 index 0000000..3a936b0 --- /dev/null +++ b/src/modules/ad/ad.module.ts @@ -0,0 +1,72 @@ +import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; +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 { Messager } from './adapters/secondaries/messager'; +import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder'; +import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider'; +import { GeorouterCreator } from '../geography/adapters/secondaries/georouter-creator'; +import { GeographyModule } from '../geography/geography.module'; +import { HttpModule } from '@nestjs/axios'; +import { PostgresDirectionEncoder } from '../geography/adapters/secondaries/postgres-direction-encoder'; + +@Module({ + imports: [ + GeographyModule, + DatabaseModule, + CqrsModule, + HttpModule, + RabbitMQModule.forRootAsync(RabbitMQModule, { + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + exchanges: [ + { + name: configService.get('RMQ_EXCHANGE'), + type: 'topic', + }, + ], + handlers: { + adCreated: { + exchange: configService.get('RMQ_EXCHANGE'), + routingKey: 'ad.created', + queue: 'matcher-ad-created', + }, + }, + uri: configService.get('RMQ_URI'), + connectionInitOptions: { wait: false }, + enableControllerDiscovery: true, + }), + inject: [ConfigService], + }), + ], + controllers: [AdMessagerController], + providers: [ + { + provide: 'ParamsProvider', + useClass: DefaultParamsProvider, + }, + { + provide: 'GeorouterCreator', + useClass: GeorouterCreator, + }, + { + provide: 'TimezoneFinder', + useClass: GeoTimezoneFinder, + }, + { + provide: 'DirectionEncoder', + useClass: PostgresDirectionEncoder, + }, + AdProfile, + Messager, + AdRepository, + CreateAdUseCase, + ], + exports: [], +}) +export class AdModule {} diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts new file mode 100644 index 0000000..ce10434 --- /dev/null +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -0,0 +1,48 @@ +import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; +import { Controller } from '@nestjs/common'; +import { CommandBus } 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'; + +@Controller() +export class AdMessagerController { + constructor( + private readonly messager: Messager, + private readonly commandBus: CommandBus, + ) {} + + @RabbitSubscribe({ + name: 'adCreated', + }) + async adCreatedHandler(message: string): Promise { + try { + // parse message to request instance + const createAdRequest: 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; + } + } + await this.commandBus.execute(new CreateAdCommand(createAdRequest)); + } catch (e) { + this.messager.publish( + 'logging.matcher.ad.crit', + JSON.stringify({ + message, + error: e, + }), + ); + } + } +} diff --git a/src/modules/ad/adapters/secondaries/ad.repository.ts b/src/modules/ad/adapters/secondaries/ad.repository.ts new file mode 100644 index 0000000..ea580c2 --- /dev/null +++ b/src/modules/ad/adapters/secondaries/ad.repository.ts @@ -0,0 +1,131 @@ +import { Injectable } from '@nestjs/common'; +import { MatcherRepository } from '../../../database/domain/matcher-repository'; +import { Ad } from '../../domain/entities/ad'; +import { DatabaseException } from '../../../database/exceptions/database.exception'; + +@Injectable() +export class AdRepository extends MatcherRepository { + protected model = 'ad'; + + async createAd(ad: Partial): Promise { + 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): Partial { + return { + 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; +}; diff --git a/src/modules/ad/adapters/secondaries/default-params.provider.ts b/src/modules/ad/adapters/secondaries/default-params.provider.ts new file mode 100644 index 0000000..f0a41fb --- /dev/null +++ b/src/modules/ad/adapters/secondaries/default-params.provider.ts @@ -0,0 +1,17 @@ +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'), + }; + }; +} diff --git a/src/modules/ad/adapters/secondaries/message-broker.ts b/src/modules/ad/adapters/secondaries/message-broker.ts new file mode 100644 index 0000000..7b4f4df --- /dev/null +++ b/src/modules/ad/adapters/secondaries/message-broker.ts @@ -0,0 +1,12 @@ +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; +} diff --git a/src/modules/ad/adapters/secondaries/messager.ts b/src/modules/ad/adapters/secondaries/messager.ts new file mode 100644 index 0000000..1888b7d --- /dev/null +++ b/src/modules/ad/adapters/secondaries/messager.ts @@ -0,0 +1,18 @@ +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('RMQ_EXCHANGE')); + } + + publish = (routingKey: string, message: string): void => { + this.amqpConnection.publish(this.exchange, routingKey, message); + }; +} diff --git a/src/modules/ad/adapters/secondaries/timezone-finder.ts b/src/modules/ad/adapters/secondaries/timezone-finder.ts new file mode 100644 index 0000000..8459661 --- /dev/null +++ b/src/modules/ad/adapters/secondaries/timezone-finder.ts @@ -0,0 +1,11 @@ +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); +} diff --git a/src/modules/ad/commands/create-ad.command.ts b/src/modules/ad/commands/create-ad.command.ts new file mode 100644 index 0000000..b4f1e8d --- /dev/null +++ b/src/modules/ad/commands/create-ad.command.ts @@ -0,0 +1,9 @@ +import { CreateAdRequest } from '../domain/dtos/create-ad.request'; + +export class CreateAdCommand { + readonly createAdRequest: CreateAdRequest; + + constructor(request: CreateAdRequest) { + this.createAdRequest = request; + } +} diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts new file mode 100644 index 0000000..58a1bb0 --- /dev/null +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -0,0 +1,140 @@ +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; +} diff --git a/src/modules/ad/domain/dtos/has-truthy-with.validator.ts b/src/modules/ad/domain/dtos/has-truthy-with.validator.ts new file mode 100644 index 0000000..06460c6 --- /dev/null +++ b/src/modules/ad/domain/dtos/has-truthy-with.validator.ts @@ -0,0 +1,32 @@ +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 here as well, if you want to make async validation + }, + }, + }); + }; +} diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts new file mode 100644 index 0000000..a3a7ffb --- /dev/null +++ b/src/modules/ad/domain/entities/ad.ts @@ -0,0 +1,109 @@ +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; +} diff --git a/src/modules/ad/domain/entities/geography.ts b/src/modules/ad/domain/entities/geography.ts new file mode 100644 index 0000000..b720399 --- /dev/null +++ b/src/modules/ad/domain/entities/geography.ts @@ -0,0 +1,92 @@ +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 => { + 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', +} diff --git a/src/modules/ad/domain/entities/time-converter.ts b/src/modules/ad/domain/entities/time-converter.ts new file mode 100644 index 0000000..e60bf76 --- /dev/null +++ b/src/modules/ad/domain/entities/time-converter.ts @@ -0,0 +1,19 @@ +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; + } + }; +} diff --git a/src/modules/ad/domain/interfaces/params-provider.interface.ts b/src/modules/ad/domain/interfaces/params-provider.interface.ts new file mode 100644 index 0000000..bde5a06 --- /dev/null +++ b/src/modules/ad/domain/interfaces/params-provider.interface.ts @@ -0,0 +1,5 @@ +import { DefaultParams } from '../types/default-params.type'; + +export interface IProvideParams { + getParams(): DefaultParams; +} diff --git a/src/modules/ad/domain/types/default-params.type.ts b/src/modules/ad/domain/types/default-params.type.ts new file mode 100644 index 0000000..bea841b --- /dev/null +++ b/src/modules/ad/domain/types/default-params.type.ts @@ -0,0 +1,5 @@ +export type DefaultParams = { + DEFAULT_TIMEZONE: string; + GEOROUTER_TYPE: string; + GEOROUTER_URL: string; +}; diff --git a/src/modules/ad/domain/types/frequency.enum.ts b/src/modules/ad/domain/types/frequency.enum.ts new file mode 100644 index 0000000..0126874 --- /dev/null +++ b/src/modules/ad/domain/types/frequency.enum.ts @@ -0,0 +1,4 @@ +export enum Frequency { + PUNCTUAL = 'PUNCTUAL', + RECURRENT = 'RECURRENT', +} diff --git a/src/modules/ad/domain/types/role.enum.ts b/src/modules/ad/domain/types/role.enum.ts new file mode 100644 index 0000000..7522f80 --- /dev/null +++ b/src/modules/ad/domain/types/role.enum.ts @@ -0,0 +1,4 @@ +export enum Role { + DRIVER = 'DRIVER', + PASSENGER = 'PASSENGER', +} diff --git a/src/modules/ad/domain/usecases/create-ad.usecase.ts b/src/modules/ad/domain/usecases/create-ad.usecase.ts new file mode 100644 index 0000000..71fcc98 --- /dev/null +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -0,0 +1,144 @@ +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 { + 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, + ); + }; +} diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts new file mode 100644 index 0000000..bbc38d5 --- /dev/null +++ b/src/modules/ad/mappers/ad.profile.ts @@ -0,0 +1,18 @@ +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); + }; + } +} diff --git a/src/modules/ad/tests/integration/ad.repository.spec.ts b/src/modules/ad/tests/integration/ad.repository.spec.ts new file mode 100644 index 0000000..1947bac --- /dev/null +++ b/src/modules/ad/tests/integration/ad.repository.spec.ts @@ -0,0 +1,402 @@ +import { TestingModule, Test } from '@nestjs/testing'; +import { DatabaseModule } from '../../../database/database.module'; +import { PrismaService } from '../../../database/adapters/secondaries/prisma-service'; +import { AdRepository } from '../../adapters/secondaries/ad.repository'; +import { Ad } from '../../domain/entities/ad'; +import { Frequency } from '../../domain/types/frequency.enum'; + +describe('AdRepository', () => { + let prismaService: PrismaService; + let adRepository: AdRepository; + + const baseUuid = { + uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00', + }; + 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 adToCreate = { + ...baseUuid, + ...baseUserUuid, + ...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 () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [DatabaseModule], + providers: [AdRepository, PrismaService], + }).compile(); + + prismaService = module.get(PrismaService); + adRepository = module.get(AdRepository); + }); + + afterAll(async () => { + await prismaService.$disconnect(); + }); + + beforeEach(async () => { + 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', () => { + it('should create an ad', async () => { + const beforeCount = await prismaService.ad.count(); + + const adToCreate: Ad = new Ad(); + adToCreate.uuid = 'be459a29-7a41-4c0b-b371-abe90bfb6f00'; + adToCreate.userUuid = '4e52b54d-a729-4dbd-9283-f84a11bb2200'; + adToCreate.driver = true; + adToCreate.passenger = false; + adToCreate.fwdAzimuth = 0; + adToCreate.backAzimuth = 180; + adToCreate.waypoints = "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'"; + adToCreate.direction = + "'LINESTRING(6 47,6.05 47.05,6.1 47.1,6.15 47.15,6.2 47.2)'"; + adToCreate.seatsDriver = 3; + adToCreate.seatsPassenger = 1; + adToCreate.seatsUsed = 0; + adToCreate.strict = false; + adToCreate.frequency = Frequency.PUNCTUAL; + adToCreate.fromDate = new Date(2023, 0, 1); + adToCreate.toDate = new Date(2023, 0, 1); + adToCreate.sunTime = new Date(2023, 0, 1, 6, 0, 0); + adToCreate.monMargin = 900; + adToCreate.tueMargin = 900; + adToCreate.wedMargin = 900; + adToCreate.thuMargin = 900; + adToCreate.friMargin = 900; + adToCreate.satMargin = 900; + adToCreate.sunMargin = 900; + const ad = await adRepository.createAd(adToCreate); + + const afterCount = await prismaService.ad.count(); + + expect(afterCount - beforeCount).toBe(1); + expect(ad.uuid).toBe('be459a29-7a41-4c0b-b371-abe90bfb6f00'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/adapters/secondaries/default-params.provider.spec.ts b/src/modules/ad/tests/unit/adapters/secondaries/default-params.provider.spec.ts new file mode 100644 index 0000000..5b69430 --- /dev/null +++ b/src/modules/ad/tests/unit/adapters/secondaries/default-params.provider.spec.ts @@ -0,0 +1,38 @@ +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider'; +import { DefaultParams } from '../../../../domain/types/default-params.type'; + +const mockConfigService = { + get: jest.fn().mockImplementation(() => 'some_default_value'), +}; + +describe('DefaultParamsProvider', () => { + let defaultParamsProvider: DefaultParamsProvider; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + DefaultParamsProvider, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + defaultParamsProvider = module.get( + DefaultParamsProvider, + ); + }); + + it('should be defined', () => { + expect(defaultParamsProvider).toBeDefined(); + }); + + it('should provide default params', async () => { + const params: DefaultParams = defaultParamsProvider.getParams(); + expect(params.GEOROUTER_URL).toBe('some_default_value'); + }); +}); diff --git a/src/modules/ad/tests/unit/adapters/secondaries/messager.spec.ts b/src/modules/ad/tests/unit/adapters/secondaries/messager.spec.ts new file mode 100644 index 0000000..0bd23a9 --- /dev/null +++ b/src/modules/ad/tests/unit/adapters/secondaries/messager.spec.ts @@ -0,0 +1,47 @@ +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); + }); + + 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); + }); +}); diff --git a/src/modules/ad/tests/unit/adapters/secondaries/timezone-finder.spec.ts b/src/modules/ad/tests/unit/adapters/secondaries/timezone-finder.spec.ts new file mode 100644 index 0000000..63d8462 --- /dev/null +++ b/src/modules/ad/tests/unit/adapters/secondaries/timezone-finder.spec.ts @@ -0,0 +1,35 @@ +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); + }); + + 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'); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts new file mode 100644 index 0000000..040c763 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts @@ -0,0 +1,176 @@ +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: { + points: [], + fwdAzimuth: 0, + backAzimuth: 180, + distance: 20000, + duration: 1800, + }, + }, + { + key: RouteKey.PASSENGER, + route: { + points: [], + fwdAzimuth: 0, + backAzimuth: 180, + distance: 20000, + duration: 1800, + }, + }, + { + key: RouteKey.COMMON, + 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 => { + createAdRequest.uuid = uuid; +}; + +const setIsDriver = async (isDriver: boolean): Promise => { + createAdRequest.driver = isDriver; +}; + +const setIsPassenger = async (isPassenger: boolean): Promise => { + 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); + }); + + 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); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/geography.spec.ts b/src/modules/ad/tests/unit/domain/geography.spec.ts new file mode 100644 index 0000000..bf3de17 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/geography.spec.ts @@ -0,0 +1,138 @@ +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); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/time-converter.spec.ts b/src/modules/ad/tests/unit/domain/time-converter.spec.ts new file mode 100644 index 0000000..9c4113d --- /dev/null +++ b/src/modules/ad/tests/unit/domain/time-converter.spec.ts @@ -0,0 +1,53 @@ +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(); + }); +}); diff --git a/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts b/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts index c9408ca..d41e817 100644 --- a/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts +++ b/src/modules/configuration/adapters/primaries/configuration-messager.controller.ts @@ -57,7 +57,7 @@ export class ConfigurationMessagerController { name: 'propagateConfiguration', }) public async propagateConfigurationsHandler(message: string) { - const configurations: Array = JSON.parse(message); + const configurations: Configuration[] = JSON.parse(message); configurations.forEach(async (configuration) => { if ( configuration.domain == diff --git a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts similarity index 85% rename from src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts rename to src/modules/database/adapters/secondaries/prisma-repository.abstract.ts index fa2ba59..635e966 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts @@ -10,7 +10,7 @@ import { PrismaService } from './prisma-service'; */ @Injectable() export abstract class PrismaRepository implements IRepository { - protected _model: string; + protected model: string; constructor(protected readonly _prisma: PrismaService) {} @@ -21,13 +21,13 @@ export abstract class PrismaRepository implements IRepository { include?: any, ): Promise> { const [data, total] = await this._prisma.$transaction([ - this._prisma[this._model].findMany({ + this._prisma[this.model].findMany({ where, include, skip: (page - 1) * perPage, take: perPage, }), - this._prisma[this._model].count({ + this._prisma[this.model].count({ where, }), ]); @@ -39,7 +39,7 @@ export abstract class PrismaRepository implements IRepository { async findOneByUuid(uuid: string): Promise { try { - const entity = await this._prisma[this._model].findUnique({ + const entity = await this._prisma[this.model].findUnique({ where: { uuid }, }); @@ -59,7 +59,7 @@ export abstract class PrismaRepository implements IRepository { async findOne(where: any, include?: any): Promise { try { - const entity = await this._prisma[this._model].findFirst({ + const entity = await this._prisma[this.model].findFirst({ where: where, include: include, }); @@ -81,7 +81,7 @@ export abstract class PrismaRepository implements IRepository { // TODO : Refactor for good clean architecture ? async create(entity: Partial | any, include?: any): Promise { try { - const res = await this._prisma[this._model].create({ + const res = await this._prisma[this.model].create({ data: entity, include: include, }); @@ -102,7 +102,7 @@ export abstract class PrismaRepository implements IRepository { async update(uuid: string, entity: Partial): Promise { try { - const updatedEntity = await this._prisma[this._model].update({ + const updatedEntity = await this._prisma[this.model].update({ where: { uuid }, data: entity, }); @@ -126,7 +126,7 @@ export abstract class PrismaRepository implements IRepository { include?: any, ): Promise { try { - const updatedEntity = await this._prisma[this._model].update({ + const updatedEntity = await this._prisma[this.model].update({ where: where, data: entity, include: include, @@ -148,7 +148,7 @@ export abstract class PrismaRepository implements IRepository { async delete(uuid: string): Promise { try { - const entity = await this._prisma[this._model].delete({ + const entity = await this._prisma[this.model].delete({ where: { uuid }, }); @@ -168,7 +168,7 @@ export abstract class PrismaRepository implements IRepository { async deleteMany(where: any): Promise { try { - const entity = await this._prisma[this._model].deleteMany({ + const entity = await this._prisma[this.model].deleteMany({ where: where, }); @@ -187,13 +187,13 @@ export abstract class PrismaRepository implements IRepository { } async findAllByQuery( - include: Array, - where: Array, + include: string[], + where: string[], ): Promise> { const query = `SELECT ${include.join(',')} FROM ${ - this._model + this.model } WHERE ${where.join(' AND ')}`; - const data: Array = await this._prisma.$queryRawUnsafe(query); + const data: T[] = await this._prisma.$queryRawUnsafe(query); return Promise.resolve({ data, total: data.length, @@ -202,9 +202,9 @@ export abstract class PrismaRepository implements IRepository { async createWithFields(fields: object): Promise { try { - const command = `INSERT INTO ${this._model} (${Object.keys(fields).join( - ',', - )}) VALUES (${Object.values(fields).join(',')})`; + 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) { @@ -219,11 +219,11 @@ export abstract class PrismaRepository implements IRepository { } } - async updateWithFields(uuid: string, entity: Partial): Promise { + async updateWithFields(uuid: string, entity: object): Promise { 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( + const command = `UPDATE ${this.model} SET ${values.join( ', ', )} WHERE uuid = '${uuid}'`; return await this._prisma.$executeRawUnsafe(command); diff --git a/src/modules/database/src/adapters/secondaries/prisma-service.ts b/src/modules/database/adapters/secondaries/prisma-service.ts similarity index 100% rename from src/modules/database/src/adapters/secondaries/prisma-service.ts rename to src/modules/database/adapters/secondaries/prisma-service.ts diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index 61328fa..5b09ac3 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; -import { PrismaService } from './src/adapters/secondaries/prisma-service'; -import { MatcherRepository } from './src/domain/matcher-repository'; +import { PrismaService } from './adapters/secondaries/prisma-service'; +import { AdRepository } from '../ad/adapters/secondaries/ad.repository'; @Module({ - providers: [PrismaService, MatcherRepository], - exports: [PrismaService, MatcherRepository], + providers: [PrismaService, AdRepository], + exports: [PrismaService, AdRepository], }) export class DatabaseModule {} diff --git a/src/modules/database/src/domain/matcher-repository.ts b/src/modules/database/domain/matcher-repository.ts similarity index 100% rename from src/modules/database/src/domain/matcher-repository.ts rename to src/modules/database/domain/matcher-repository.ts diff --git a/src/modules/database/src/domain/point.type.ts b/src/modules/database/domain/point.type.ts similarity index 100% rename from src/modules/database/src/domain/point.type.ts rename to src/modules/database/domain/point.type.ts diff --git a/src/modules/database/src/exceptions/database.exception.ts b/src/modules/database/exceptions/database.exception.ts similarity index 100% rename from src/modules/database/src/exceptions/database.exception.ts rename to src/modules/database/exceptions/database.exception.ts diff --git a/src/modules/database/src/interfaces/collection.interface.ts b/src/modules/database/interfaces/collection.interface.ts similarity index 100% rename from src/modules/database/src/interfaces/collection.interface.ts rename to src/modules/database/interfaces/collection.interface.ts diff --git a/src/modules/database/src/interfaces/repository.interface.ts b/src/modules/database/interfaces/repository.interface.ts similarity index 100% rename from src/modules/database/src/interfaces/repository.interface.ts rename to src/modules/database/interfaces/repository.interface.ts diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index 1b0e1f7..eb3bad0 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaService } from '../../src/adapters/secondaries/prisma-service'; -import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract'; -import { DatabaseException } from '../../src/exceptions/database.exception'; +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 { @@ -41,7 +41,7 @@ Array.from({ length: 10 }).forEach(() => { @Injectable() class FakePrismaRepository extends PrismaRepository { - protected _model = 'fake'; + protected model = 'fake'; } class FakePrismaService extends PrismaService { diff --git a/src/modules/geography/adapters/secondaries/geo-timezone-finder.ts b/src/modules/geography/adapters/secondaries/geo-timezone-finder.ts new file mode 100644 index 0000000..bce0097 --- /dev/null +++ b/src/modules/geography/adapters/secondaries/geo-timezone-finder.ts @@ -0,0 +1,8 @@ +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); +} diff --git a/src/modules/geography/adapters/secondaries/geodesic.ts b/src/modules/geography/adapters/secondaries/geodesic.ts new file mode 100644 index 0000000..835df8e --- /dev/null +++ b/src/modules/geography/adapters/secondaries/geodesic.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { Geodesic as Geolib, GeodesicClass } from 'geographiclib-geodesic'; +import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; + +@Injectable() +export class Geodesic implements IGeodesic { + private geod: GeodesicClass; + + constructor() { + this.geod = Geolib.WGS84; + } + + inverse = ( + lon1: number, + lat1: number, + lon2: number, + lat2: number, + ): { azimuth: number; distance: number } => { + const { azi2: azimuth, s12: distance } = this.geod.Inverse( + lat1, + lon1, + lat2, + lon2, + ); + return { azimuth, distance }; + }; +} diff --git a/src/modules/geography/adapters/secondaries/georouter-creator.ts b/src/modules/geography/adapters/secondaries/georouter-creator.ts new file mode 100644 index 0000000..f147a62 --- /dev/null +++ b/src/modules/geography/adapters/secondaries/georouter-creator.ts @@ -0,0 +1,28 @@ +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', + ); + } + }; +} diff --git a/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts b/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts new file mode 100644 index 0000000..fd83d2b --- /dev/null +++ b/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts @@ -0,0 +1,330 @@ +import { HttpService } from '@nestjs/axios'; +import { IGeorouter } from '../../domain/interfaces/georouter.interface'; +import { Injectable } from '@nestjs/common'; +import { catchError, lastValueFrom, map } from 'rxjs'; +import { AxiosError, AxiosResponse } from 'axios'; +import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface'; +import { GeorouterSettings } from '../../domain/types/georouter-settings.type'; +import { Path } from '../../domain/types/path.type'; +import { NamedRoute } from '../../domain/types/named-route'; +import { GeographyException } from '../../exceptions/geography.exception'; +import { ExceptionCode } from '../../..//utils/exception-code.enum'; +import { Route } from '../../domain/entities/route'; +import { SpacetimePoint } from '../../domain/entities/spacetime-point'; + +@Injectable() +export class GraphhopperGeorouter implements IGeorouter { + private url: string; + private urlArgs: string[]; + private withTime: boolean; + private withPoints: boolean; + private withDistance: boolean; + private paths: Path[]; + private httpService: HttpService; + private geodesic: IGeodesic; + + constructor(url: string, httpService: HttpService, geodesic: IGeodesic) { + this.url = url + '/route?'; + this.httpService = httpService; + this.geodesic = geodesic; + } + + route = async ( + paths: Path[], + settings: GeorouterSettings, + ): Promise => { + this.setDefaultUrlArgs(); + this.setWithTime(settings.withTime); + this.setWithPoints(settings.withPoints); + this.setWithDistance(settings.withDistance); + this.paths = paths; + return await this.getRoutes(); + }; + + private setDefaultUrlArgs = (): void => { + this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false']; + }; + + private setWithTime = (withTime: boolean): void => { + this.withTime = withTime; + if (withTime) { + this.urlArgs.push('details=time'); + } + }; + + private setWithPoints = (withPoints: boolean): void => { + this.withPoints = withPoints; + if (!withPoints) { + this.urlArgs.push('calc_points=false'); + } + }; + + private setWithDistance = (withDistance: boolean): void => { + this.withDistance = withDistance; + if (withDistance) { + this.urlArgs.push('instructions=true'); + } else { + this.urlArgs.push('instructions=false'); + } + }; + + private getRoutes = async (): Promise => { + const routes = Promise.all( + this.paths.map(async (path) => { + const url: string = [ + this.getUrl(), + '&point=', + path.points + .map((point) => [point.lat, point.lon].join('%2C')) + .join('&point='), + ].join(''); + const route = await lastValueFrom( + this.httpService.get(url).pipe( + map((res) => (res.data ? this.createRoute(res) : undefined)), + catchError((error: AxiosError) => { + if (error.code == AxiosError.ERR_BAD_REQUEST) { + throw new GeographyException( + ExceptionCode.OUT_OF_RANGE, + 'No route found for given coordinates', + ); + } + throw new GeographyException( + ExceptionCode.UNAVAILABLE, + 'Georouter unavailable : ' + error.message, + ); + }), + ), + ); + return { + key: path.key, + route, + }; + }), + ); + return routes; + }; + + private getUrl = (): string => { + return [this.url, this.urlArgs.join('&')].join(''); + }; + + private createRoute = ( + response: AxiosResponse, + ): Route => { + const route = new Route(this.geodesic); + if (response.data.paths && response.data.paths[0]) { + const shortestPath = response.data.paths[0]; + route.distance = shortestPath.distance ?? 0; + route.duration = shortestPath.time ? shortestPath.time / 1000 : 0; + if (shortestPath.points && shortestPath.points.coordinates) { + route.setPoints( + shortestPath.points.coordinates.map((coordinate) => ({ + lon: coordinate[0], + lat: coordinate[1], + })), + ); + if ( + shortestPath.details && + shortestPath.details.time && + shortestPath.snapped_waypoints && + shortestPath.snapped_waypoints.coordinates + ) { + let instructions: GraphhopperInstruction[] = []; + if (shortestPath.instructions) + instructions = shortestPath.instructions; + route.setSpacetimePoints( + this.generateSpacetimePoints( + shortestPath.points.coordinates, + shortestPath.snapped_waypoints.coordinates, + shortestPath.details.time, + instructions, + ), + ); + } + } + } + return route; + }; + + private generateSpacetimePoints = ( + points: Array, + snappedWaypoints: Array, + durations: Array, + instructions: GraphhopperInstruction[], + ): SpacetimePoint[] => { + const indices = this.getIndices(points, snappedWaypoints); + const times = this.getTimes(durations, indices); + const distances = this.getDistances(instructions, indices); + return indices.map( + (index) => + new SpacetimePoint( + { lon: points[index][1], lat: points[index][0] }, + times.find((time) => time.index == index)?.duration, + distances.find((distance) => distance.index == index)?.distance, + ), + ); + }; + + private getIndices = ( + points: Array, + snappedWaypoints: Array, + ): number[] => { + const indices = snappedWaypoints.map((waypoint) => + points.findIndex( + (point) => point[0] == waypoint[0] && point[1] == waypoint[1], + ), + ); + if (indices.find((index) => index == -1) === undefined) return indices; + const missedWaypoints = indices + .map( + (value, index) => + < + { + index: number; + originIndex: number; + waypoint: number[]; + nearest: number; + distance: number; + } + >{ + index: value, + originIndex: index, + waypoint: snappedWaypoints[index], + nearest: undefined, + distance: 999999999, + }, + ) + .filter((element) => element.index == -1); + for (const index in points) { + for (const missedWaypoint of missedWaypoints) { + const inverse = this.geodesic.inverse( + missedWaypoint.waypoint[0], + missedWaypoint.waypoint[1], + points[index][0], + points[index][1], + ); + if (inverse.distance < missedWaypoint.distance) { + missedWaypoint.distance = inverse.distance; + missedWaypoint.nearest = parseInt(index); + } + } + } + for (const missedWaypoint of missedWaypoints) { + indices[missedWaypoint.originIndex] = missedWaypoint.nearest; + } + return indices; + }; + + private getTimes = ( + durations: Array, + indices: number[], + ): Array<{ index: number; duration: number }> => { + const times: Array<{ index: number; duration: number }> = []; + let duration = 0; + for (const [origin, destination, stepDuration] of durations) { + let indexFound = false; + const indexAsOrigin = indices.find((index) => index == origin); + if ( + indexAsOrigin !== undefined && + times.find((time) => origin == time.index) == undefined + ) { + times.push({ + index: indexAsOrigin, + duration: Math.round(stepDuration / 1000), + }); + indexFound = true; + } + if (!indexFound) { + const indexAsDestination = indices.find( + (index) => index == destination, + ); + if ( + indexAsDestination !== undefined && + times.find((time) => destination == time.index) == undefined + ) { + times.push({ + index: indexAsDestination, + duration: Math.round((duration + stepDuration) / 1000), + }); + indexFound = true; + } + } + if (!indexFound) { + const indexInBetween = indices.find( + (index) => origin < index && index < destination, + ); + if (indexInBetween !== undefined) { + times.push({ + index: indexInBetween, + duration: Math.round((duration + stepDuration / 2) / 1000), + }); + } + } + duration += stepDuration; + } + return times; + }; + + private getDistances = ( + instructions: GraphhopperInstruction[], + indices: number[], + ): Array<{ index: number; distance: number }> => { + let distance = 0; + const distances: Array<{ index: number; distance: number }> = [ + { + index: 0, + distance, + }, + ]; + for (const instruction of instructions) { + distance += instruction.distance; + if ( + (instruction.sign == GraphhopperSign.SIGN_WAYPOINT || + instruction.sign == GraphhopperSign.SIGN_FINISH) && + indices.find((index) => index == instruction.interval[0]) !== undefined + ) { + distances.push({ + index: instruction.interval[0], + distance: Math.round(distance), + }); + } + } + return distances; + }; +} + +type GraphhopperResponse = { + paths: [ + { + distance: number; + weight: number; + time: number; + points_encoded: boolean; + bbox: number[]; + points: GraphhopperCoordinates; + snapped_waypoints: GraphhopperCoordinates; + details: { + time: Array; + }; + instructions: GraphhopperInstruction[]; + }, + ]; +}; + +type GraphhopperCoordinates = { + coordinates: Array; +}; + +type GraphhopperInstruction = { + distance: number; + heading: number; + sign: GraphhopperSign; + interval: number[]; + text: string; +}; + +enum GraphhopperSign { + SIGN_START = 0, + SIGN_FINISH = 4, + SIGN_WAYPOINT = 5, +} diff --git a/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts b/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts new file mode 100644 index 0000000..3a85ace --- /dev/null +++ b/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts @@ -0,0 +1,11 @@ +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(''); +} diff --git a/src/modules/geography/domain/entities/coordinate.ts b/src/modules/geography/domain/entities/coordinate.ts new file mode 100644 index 0000000..4dd416a --- /dev/null +++ b/src/modules/geography/domain/entities/coordinate.ts @@ -0,0 +1,19 @@ +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; +} diff --git a/src/modules/matcher/domain/entities/ecosystem/route.ts b/src/modules/geography/domain/entities/route.ts similarity index 62% rename from src/modules/matcher/domain/entities/ecosystem/route.ts rename to src/modules/geography/domain/entities/route.ts index d2b1238..48b8744 100644 --- a/src/modules/matcher/domain/entities/ecosystem/route.ts +++ b/src/modules/geography/domain/entities/route.ts @@ -1,7 +1,6 @@ -import { IGeodesic } from '../../interfaces/geodesic.interface'; -import { Point } from '../../types/point.type'; +import { IGeodesic } from '../interfaces/geodesic.interface'; +import { Point } from '../types/point.type'; import { SpacetimePoint } from './spacetime-point'; -import { Waypoint } from './waypoint'; export class Route { distance: number; @@ -9,9 +8,8 @@ export class Route { fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - waypoints: Array; - points: Array; - spacetimePoints: Array; + points: Point[]; + spacetimePoints: SpacetimePoint[]; private geodesic: IGeodesic; constructor(geodesic: IGeodesic) { @@ -20,27 +18,21 @@ export class Route { this.fwdAzimuth = undefined; this.backAzimuth = undefined; this.distanceAzimuth = undefined; - this.waypoints = []; this.points = []; this.spacetimePoints = []; this.geodesic = geodesic; } - setWaypoints = (waypoints: Array): void => { - this.waypoints = waypoints; - this.setAzimuth(waypoints.map((waypoint) => waypoint.point)); - }; - - setPoints = (points: Array): void => { + setPoints = (points: Point[]): void => { this.points = points; this.setAzimuth(points); }; - setSpacetimePoints = (spacetimePoints: Array): void => { + setSpacetimePoints = (spacetimePoints: SpacetimePoint[]): void => { this.spacetimePoints = spacetimePoints; }; - private setAzimuth = (points: Array): void => { + protected setAzimuth = (points: Point[]): void => { const inverse = this.geodesic.inverse( points[0].lon, points[0].lat, diff --git a/src/modules/geography/domain/entities/spacetime-point.ts b/src/modules/geography/domain/entities/spacetime-point.ts new file mode 100644 index 0000000..7d720e1 --- /dev/null +++ b/src/modules/geography/domain/entities/spacetime-point.ts @@ -0,0 +1,13 @@ +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; + } +} diff --git a/src/modules/geography/domain/interfaces/direction-encoder.interface.ts b/src/modules/geography/domain/interfaces/direction-encoder.interface.ts new file mode 100644 index 0000000..0f38a49 --- /dev/null +++ b/src/modules/geography/domain/interfaces/direction-encoder.interface.ts @@ -0,0 +1,5 @@ +import { Coordinate } from '../entities/coordinate'; + +export interface IEncodeDirection { + encode(coordinates: Coordinate[]): string; +} diff --git a/src/modules/matcher/domain/interfaces/geodesic.interface.ts b/src/modules/geography/domain/interfaces/geodesic.interface.ts similarity index 100% rename from src/modules/matcher/domain/interfaces/geodesic.interface.ts rename to src/modules/geography/domain/interfaces/geodesic.interface.ts diff --git a/src/modules/geography/domain/interfaces/georouter-creator.interface.ts b/src/modules/geography/domain/interfaces/georouter-creator.interface.ts new file mode 100644 index 0000000..7a6bd25 --- /dev/null +++ b/src/modules/geography/domain/interfaces/georouter-creator.interface.ts @@ -0,0 +1,5 @@ +import { IGeorouter } from './georouter.interface'; + +export interface ICreateGeorouter { + create(type: string, url: string): IGeorouter; +} diff --git a/src/modules/geography/domain/interfaces/georouter.interface.ts b/src/modules/geography/domain/interfaces/georouter.interface.ts new file mode 100644 index 0000000..c2c2e05 --- /dev/null +++ b/src/modules/geography/domain/interfaces/georouter.interface.ts @@ -0,0 +1,7 @@ +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; +} diff --git a/src/modules/geography/domain/interfaces/timezone-finder.interface.ts b/src/modules/geography/domain/interfaces/timezone-finder.interface.ts new file mode 100644 index 0000000..61016f7 --- /dev/null +++ b/src/modules/geography/domain/interfaces/timezone-finder.interface.ts @@ -0,0 +1,3 @@ +export interface IFindTimezone { + timezones(lon: number, lat: number): string[]; +} diff --git a/src/modules/geography/domain/types/georouter-settings.type.ts b/src/modules/geography/domain/types/georouter-settings.type.ts new file mode 100644 index 0000000..d8f73ae --- /dev/null +++ b/src/modules/geography/domain/types/georouter-settings.type.ts @@ -0,0 +1,5 @@ +export type GeorouterSettings = { + withPoints: boolean; + withTime: boolean; + withDistance: boolean; +}; diff --git a/src/modules/geography/domain/types/named-route.ts b/src/modules/geography/domain/types/named-route.ts new file mode 100644 index 0000000..f1fdb2d --- /dev/null +++ b/src/modules/geography/domain/types/named-route.ts @@ -0,0 +1,6 @@ +import { Route } from '../entities/route'; + +export type NamedRoute = { + key: string; + route: Route; +}; diff --git a/src/modules/geography/domain/types/path.type.ts b/src/modules/geography/domain/types/path.type.ts new file mode 100644 index 0000000..44e03b6 --- /dev/null +++ b/src/modules/geography/domain/types/path.type.ts @@ -0,0 +1,6 @@ +import { Point } from '../../../geography/domain/types/point.type'; + +export type Path = { + key: string; + points: Point[]; +}; diff --git a/src/modules/matcher/domain/types/geography.enum.ts b/src/modules/geography/domain/types/point-type.enum.ts similarity index 100% rename from src/modules/matcher/domain/types/geography.enum.ts rename to src/modules/geography/domain/types/point-type.enum.ts diff --git a/src/modules/geography/domain/types/point.type.ts b/src/modules/geography/domain/types/point.type.ts new file mode 100644 index 0000000..37c49e6 --- /dev/null +++ b/src/modules/geography/domain/types/point.type.ts @@ -0,0 +1,6 @@ +import { PointType } from './point-type.enum'; +import { Coordinate } from '../entities/coordinate'; + +export type Point = Coordinate & { + type?: PointType; +}; diff --git a/src/modules/geography/domain/types/timezoner.ts b/src/modules/geography/domain/types/timezoner.ts new file mode 100644 index 0000000..8764400 --- /dev/null +++ b/src/modules/geography/domain/types/timezoner.ts @@ -0,0 +1,6 @@ +import { IFindTimezone } from '../interfaces/timezone-finder.interface'; + +export type Timezoner = { + timezone: string; + finder: IFindTimezone; +}; diff --git a/src/modules/geography/exceptions/geography.exception.ts b/src/modules/geography/exceptions/geography.exception.ts new file mode 100644 index 0000000..ebc1813 --- /dev/null +++ b/src/modules/geography/exceptions/geography.exception.ts @@ -0,0 +1,11 @@ +export class GeographyException implements Error { + name: string; + code: number; + message: string; + + constructor(code: number, message: string) { + this.name = 'GeographyException'; + this.code = code; + this.message = message; + } +} diff --git a/src/modules/geography/geography.module.ts b/src/modules/geography/geography.module.ts new file mode 100644 index 0000000..d4be2c8 --- /dev/null +++ b/src/modules/geography/geography.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { GeoTimezoneFinder } from './adapters/secondaries/geo-timezone-finder'; +import { Geodesic } from './adapters/secondaries/geodesic'; + +@Module({ + providers: [GeoTimezoneFinder, Geodesic], + exports: [GeoTimezoneFinder, Geodesic], +}) +export class GeographyModule {} diff --git a/src/modules/geography/tests/unit/coordinate.spec.ts b/src/modules/geography/tests/unit/coordinate.spec.ts new file mode 100644 index 0000000..6bc92e1 --- /dev/null +++ b/src/modules/geography/tests/unit/coordinate.spec.ts @@ -0,0 +1,8 @@ +import { Coordinate } from '../../domain/entities/coordinate'; + +describe('Coordinate entity', () => { + it('should be defined', () => { + const coordinate: Coordinate = new Coordinate(6, 47); + expect(coordinate).toBeDefined(); + }); +}); diff --git a/src/modules/geography/tests/unit/geo-timezone-finder.spec.ts b/src/modules/geography/tests/unit/geo-timezone-finder.spec.ts new file mode 100644 index 0000000..285761f --- /dev/null +++ b/src/modules/geography/tests/unit/geo-timezone-finder.spec.ts @@ -0,0 +1,14 @@ +import { GeoTimezoneFinder } from '../../adapters/secondaries/geo-timezone-finder'; + +describe('Geo TZ Finder', () => { + it('should be defined', () => { + const timezoneFinder: GeoTimezoneFinder = new GeoTimezoneFinder(); + expect(timezoneFinder).toBeDefined(); + }); + it('should get timezone for Nancy(France) as Europe/Paris', () => { + const timezoneFinder: GeoTimezoneFinder = new GeoTimezoneFinder(); + const timezones = timezoneFinder.timezones(6.179373, 48.687913); + expect(timezones.length).toBe(1); + expect(timezones[0]).toBe('Europe/Paris'); + }); +}); diff --git a/src/modules/geography/tests/unit/geodesic.spec.ts b/src/modules/geography/tests/unit/geodesic.spec.ts new file mode 100644 index 0000000..750d7d4 --- /dev/null +++ b/src/modules/geography/tests/unit/geodesic.spec.ts @@ -0,0 +1,14 @@ +import { Geodesic } from '../../adapters/secondaries/geodesic'; + +describe('Matcher geodesic', () => { + it('should be defined', () => { + const geodesic: Geodesic = new Geodesic(); + expect(geodesic).toBeDefined(); + }); + it('should get inverse values', () => { + const geodesic: Geodesic = new Geodesic(); + const inv = geodesic.inverse(0, 0, 1, 1); + expect(Math.round(inv.azimuth)).toBe(45); + expect(Math.round(inv.distance)).toBe(156900); + }); +}); diff --git a/src/modules/geography/tests/unit/georouter-creator.spec.ts b/src/modules/geography/tests/unit/georouter-creator.spec.ts new file mode 100644 index 0000000..03afe6c --- /dev/null +++ b/src/modules/geography/tests/unit/georouter-creator.spec.ts @@ -0,0 +1,47 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpService } from '@nestjs/axios'; +import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; +import { Geodesic } from '../../adapters/secondaries/geodesic'; +import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter'; + +const mockHttpService = jest.fn(); +const mockGeodesic = jest.fn(); + +describe('Georouter creator', () => { + let georouterCreator: GeorouterCreator; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + GeorouterCreator, + { + provide: HttpService, + useValue: mockHttpService, + }, + { + provide: Geodesic, + useValue: mockGeodesic, + }, + ], + }).compile(); + + georouterCreator = module.get(GeorouterCreator); + }); + + it('should be defined', () => { + expect(georouterCreator).toBeDefined(); + }); + it('should create a graphhopper georouter', () => { + const georouter = georouterCreator.create( + 'graphhopper', + 'http://localhost', + ); + expect(georouter).toBeInstanceOf(GraphhopperGeorouter); + }); + it('should throw an exception if georouter type is unknown', () => { + expect(() => + georouterCreator.create('unknown', 'http://localhost'), + ).toThrow(); + }); +}); diff --git a/src/modules/geography/tests/unit/graphhopper-georouter.spec.ts b/src/modules/geography/tests/unit/graphhopper-georouter.spec.ts new file mode 100644 index 0000000..e0e45f9 --- /dev/null +++ b/src/modules/geography/tests/unit/graphhopper-georouter.spec.ts @@ -0,0 +1,456 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpService } from '@nestjs/axios'; +import { of } from 'rxjs'; +import { AxiosError } from 'axios'; +import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator'; +import { IGeorouter } from '../../domain/interfaces/georouter.interface'; +import { Geodesic } from '../../adapters/secondaries/geodesic'; + +const mockHttpService = { + get: jest + .fn() + .mockImplementationOnce(() => { + throw new AxiosError('Axios error !'); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + details: { + time: [ + [0, 1, 180000], + [1, 2, 180000], + [2, 3, 180000], + [3, 4, 180000], + [4, 5, 180000], + [5, 6, 180000], + [6, 7, 180000], + [7, 9, 360000], + [9, 10, 180000], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [10, 10], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [5, 5], + [10, 10], + ], + }, + details: { + time: [ + [0, 1, 180000], + [1, 2, 180000], + [2, 3, 180000], + [3, 4, 180000], + [4, 7, 540000], + [7, 9, 360000], + [9, 10, 180000], + ], + }, + }, + ], + }, + }); + }) + .mockImplementationOnce(() => { + return of({ + status: 200, + data: { + paths: [ + { + distance: 50000, + time: 1800000, + points: { + coordinates: [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + [10, 10], + ], + }, + snapped_waypoints: { + coordinates: [ + [0, 0], + [5, 5], + [10, 10], + ], + }, + details: { + time: [ + [0, 1, 180000], + [1, 2, 180000], + [2, 3, 180000], + [3, 4, 180000], + [4, 7, 540000], + [7, 9, 360000], + [9, 10, 180000], + ], + }, + instructions: [ + { + distance: 25000, + sign: 0, + interval: [0, 5], + text: 'Some instructions', + time: 900000, + }, + { + distance: 0, + sign: 5, + interval: [5, 5], + text: 'Waypoint 1', + time: 0, + }, + { + distance: 25000, + sign: 2, + interval: [5, 10], + text: 'Some instructions', + time: 900000, + }, + { + distance: 0.0, + sign: 4, + interval: [10, 10], + text: 'Arrive at destination', + time: 0, + }, + ], + }, + ], + }, + }); + }), +}; + +const mockGeodesic = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inverse: jest.fn().mockImplementation(() => ({ + azimuth: 45, + distance: 50000, + })), +}; + +describe('Graphhopper Georouter', () => { + let georouterCreator: GeorouterCreator; + let graphhopperGeorouter: IGeorouter; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + GeorouterCreator, + { + provide: HttpService, + useValue: mockHttpService, + }, + { + provide: Geodesic, + useValue: mockGeodesic, + }, + ], + }).compile(); + + georouterCreator = module.get(GeorouterCreator); + graphhopperGeorouter = georouterCreator.create( + 'graphhopper', + 'http://localhost', + ); + }); + + it('should be defined', () => { + expect(graphhopperGeorouter).toBeDefined(); + }); + + describe('route function', () => { + it('should fail on axios error', async () => { + await expect( + graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 1, + lon: 1, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ), + ).rejects.toBeInstanceOf(Error); + }); + + it('should create one route with all settings to false', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: false, + withTime: false, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.distance).toBe(50000); + }); + + it('should create one route with points', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: false, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.distance).toBe(50000); + expect(routes[0].route.duration).toBe(1800); + expect(routes[0].route.fwdAzimuth).toBe(45); + expect(routes[0].route.backAzimuth).toBe(225); + expect(routes[0].route.points.length).toBe(11); + }); + + it('should create one route with points and time', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: true, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.spacetimePoints.length).toBe(2); + expect(routes[0].route.spacetimePoints[1].duration).toBe(1800); + expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined(); + }); + + it('should create one route with points and missed waypoints extrapolations', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 5, + lon: 5, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: false, + withPoints: true, + withTime: true, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.spacetimePoints.length).toBe(3); + expect(routes[0].route.distance).toBe(50000); + expect(routes[0].route.duration).toBe(1800); + expect(routes[0].route.fwdAzimuth).toBe(45); + expect(routes[0].route.backAzimuth).toBe(225); + expect(routes[0].route.points.length).toBe(9); + }); + + it('should create one route with points, time and distance', async () => { + const routes = await graphhopperGeorouter.route( + [ + { + key: 'route1', + points: [ + { + lat: 0, + lon: 0, + }, + { + lat: 10, + lon: 10, + }, + ], + }, + ], + { + withDistance: true, + withPoints: true, + withTime: true, + }, + ); + expect(routes).toHaveLength(1); + expect(routes[0].route.spacetimePoints.length).toBe(3); + expect(routes[0].route.spacetimePoints[1].duration).toBe(990); + expect(routes[0].route.spacetimePoints[1].distance).toBe(25000); + }); + }); +}); diff --git a/src/modules/geography/tests/unit/postgres-direction-encoder.spec.ts b/src/modules/geography/tests/unit/postgres-direction-encoder.spec.ts new file mode 100644 index 0000000..71b8ea3 --- /dev/null +++ b/src/modules/geography/tests/unit/postgres-direction-encoder.spec.ts @@ -0,0 +1,30 @@ +import { PostgresDirectionEncoder } from '../../adapters/secondaries/postgres-direction-encoder'; +import { Coordinate } from '../../domain/entities/coordinate'; + +describe('Postgres direction encoder', () => { + it('should be defined', () => { + const postgresDirectionEncoder: PostgresDirectionEncoder = + new PostgresDirectionEncoder(); + expect(postgresDirectionEncoder).toBeDefined(); + }); + it('should encode coordinates to a postgres direction', () => { + const postgresDirectionEncoder: PostgresDirectionEncoder = + new PostgresDirectionEncoder(); + const coordinates: Coordinate[] = [ + { + lon: 6, + lat: 47, + }, + { + lon: 6.1, + lat: 47.1, + }, + { + lon: 6.2, + lat: 47.2, + }, + ]; + const direction = postgresDirectionEncoder.encode(coordinates); + expect(direction).toBe("'LINESTRING(6 47,6.1 47.1,6.2 47.2)'"); + }); +}); diff --git a/src/modules/geography/tests/unit/route.spec.ts b/src/modules/geography/tests/unit/route.spec.ts new file mode 100644 index 0000000..7a8c1e4 --- /dev/null +++ b/src/modules/geography/tests/unit/route.spec.ts @@ -0,0 +1,48 @@ +import { Route } from '../../domain/entities/route'; +import { SpacetimePoint } from '../../domain/entities/spacetime-point'; + +const mockGeodesic = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + inverse: jest.fn().mockImplementation((lon1, lat1, lon2, lat2) => { + return lon1 == 0 + ? { + azimuth: 45, + distance: 50000, + } + : { + azimuth: -45, + distance: 60000, + }; + }), +}; + +describe('Route entity', () => { + it('should be defined', () => { + const route = new Route(mockGeodesic); + expect(route).toBeDefined(); + }); + it('should set points and geodesic values for a route', () => { + const route = new Route(mockGeodesic); + route.setPoints([ + { + lon: 10, + lat: 10, + }, + { + lon: 20, + lat: 20, + }, + ]); + expect(route.points.length).toBe(2); + expect(route.fwdAzimuth).toBe(315); + expect(route.backAzimuth).toBe(135); + expect(route.distanceAzimuth).toBe(60000); + }); + it('should set spacetimePoints for a route', () => { + const route = new Route(mockGeodesic); + const spacetimePoint1 = new SpacetimePoint({ lon: 0, lat: 0 }, 0, 0); + const spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000); + route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]); + expect(route.spacetimePoints.length).toBe(2); + }); +}); diff --git a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts index 0b788eb..cb04b3d 100644 --- a/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts +++ b/src/modules/health/domain/usecases/prisma.health-indicator.usecase.ts @@ -4,7 +4,7 @@ import { HealthIndicator, HealthIndicatorResult, } from '@nestjs/terminus'; -import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository'; +import { AdRepository } from '../../../ad/adapters/secondaries/ad.repository'; @Injectable() export class PrismaHealthIndicatorUseCase extends HealthIndicator { diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts index db4980d..3a62c64 100644 --- a/src/modules/health/health.module.ts +++ b/src/modules/health/health.module.ts @@ -7,7 +7,7 @@ import { TerminusModule } from '@nestjs/terminus'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { Messager } from './adapters/secondaries/messager'; -import { AdRepository } from '../matcher/adapters/secondaries/ad.repository'; +import { AdRepository } from '../ad/adapters/secondaries/ad.repository'; @Module({ imports: [ diff --git a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts index 7d3cf42..8c30654 100644 --- a/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts +++ b/src/modules/health/tests/unit/prisma.health-indicator.usecase.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase'; import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus'; -import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository'; +import { AdRepository } from '../../../ad/adapters/secondaries/ad.repository'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; const mockAdRepository = { diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index d1859ff..bc926b2 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -3,14 +3,16 @@ import { InjectMapper } from '@automapper/nestjs'; import { Controller, UsePipes } from '@nestjs/common'; import { QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { RpcValidationPipe } from 'src/modules/utils/pipes/rpc.validation-pipe'; +import { RpcValidationPipe } from '../../../utils/pipes/rpc.validation-pipe'; import { MatchRequest } from '../../domain/dtos/match.request'; -import { ICollection } from 'src/modules/database/src/interfaces/collection.interface'; +import { ICollection } from '../../../database/interfaces/collection.interface'; import { MatchQuery } from '../../queries/match.query'; import { MatchPresenter } from '../secondaries/match.presenter'; import { DefaultParamsProvider } from '../secondaries/default-params.provider'; import { GeorouterCreator } from '../secondaries/georouter-creator'; import { Match } from '../../domain/entities/ecosystem/match'; +import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder'; +import { TimeConverter } from '../secondaries/time-converter'; @UsePipes( new RpcValidationPipe({ @@ -21,25 +23,29 @@ import { Match } from '../../domain/entities/ecosystem/match'; @Controller() export class MatcherController { constructor( - private readonly _queryBus: QueryBus, - private readonly _defaultParamsProvider: DefaultParamsProvider, - @InjectMapper() private readonly _mapper: Mapper, - private readonly _georouterCreator: GeorouterCreator, + private readonly queryBus: QueryBus, + private readonly defaultParamsProvider: DefaultParamsProvider, + @InjectMapper() private readonly mapper: Mapper, + private readonly georouterCreator: GeorouterCreator, + private readonly timezoneFinder: GeoTimezoneFinder, + private readonly timeConverter: TimeConverter, ) {} @GrpcMethod('MatcherService', 'Match') async match(data: MatchRequest): Promise> { try { - const matchCollection = await this._queryBus.execute( + const matchCollection = await this.queryBus.execute( new MatchQuery( data, - this._defaultParamsProvider.getParams(), - this._georouterCreator, + this.defaultParamsProvider.getParams(), + this.georouterCreator, + this.timezoneFinder, + this.timeConverter, ), ); return Promise.resolve({ data: matchCollection.data.map((match: Match) => - this._mapper.map(match, Match, MatchPresenter), + this.mapper.map(match, Match, MatchPresenter), ), total: matchCollection.total, }); diff --git a/src/modules/matcher/adapters/primaries/matcher.proto b/src/modules/matcher/adapters/primaries/matcher.proto index af4e083..898e6ee 100644 --- a/src/modules/matcher/adapters/primaries/matcher.proto +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -7,31 +7,31 @@ service MatcherService { } message MatchRequest { - repeated Point waypoints = 1; - string departure = 2; - string fromDate = 3; - Schedule schedule = 4; - bool driver = 5; - bool passenger = 6; - string toDate = 7; - int32 marginDuration = 8; - MarginDurations marginDurations = 9; - int32 seatsPassenger = 10; - int32 seatsDriver = 11; - bool strict = 12; - Algorithm algorithm = 13; - int32 remoteness = 14; - bool useProportion = 15; - int32 proportion = 16; - bool useAzimuth = 17; - int32 azimuthMargin = 18; - float maxDetourDistanceRatio = 19; - float maxDetourDurationRatio = 20; - repeated int32 exclusions = 21; - int32 identifier = 22; + string uuid = 1; + repeated Coordinates waypoints = 2; + string departure = 3; + string fromDate = 4; + Schedule schedule = 5; + bool driver = 6; + bool passenger = 7; + string toDate = 8; + int32 marginDuration = 9; + MarginDurations marginDurations = 10; + int32 seatsPassenger = 11; + int32 seatsDriver = 12; + bool strict = 13; + Algorithm algorithm = 14; + int32 remoteness = 15; + bool useProportion = 16; + int32 proportion = 17; + bool useAzimuth = 18; + int32 azimuthMargin = 19; + float maxDetourDistanceRatio = 20; + float maxDetourDurationRatio = 21; + repeated int32 exclusions = 22; } -message Point { +message Coordinates { float lon = 1; float lat = 2; } diff --git a/src/modules/matcher/adapters/secondaries/ad.repository.ts b/src/modules/matcher/adapters/secondaries/ad.repository.ts deleted file mode 100644 index 9915f1f..0000000 --- a/src/modules/matcher/adapters/secondaries/ad.repository.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { MatcherRepository } from '../../../database/src/domain/matcher-repository'; -import { Ad } from '../../domain/entities/ecosystem/ad'; - -@Injectable() -export class AdRepository extends MatcherRepository { - protected _model = 'ad'; -} diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index c67dc10..d331919 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -8,29 +8,27 @@ export class DefaultParamsProvider { getParams = (): IDefaultParams => { return { - DEFAULT_IDENTIFIER: parseInt( - this.configService.get('DEFAULT_IDENTIFIER'), - ), + DEFAULT_UUID: this.configService.get('DEFAULT_UUID'), MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')), VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')), DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'), DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')), DEFAULT_ALGORITHM_SETTINGS: { - algorithm: this.configService.get('ALGORITHM'), - strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')), - remoteness: parseInt(this.configService.get('REMOTENESS')), - useProportion: !!parseInt(this.configService.get('USE_PROPORTION')), - proportion: parseInt(this.configService.get('PROPORTION')), - useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')), - azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')), - maxDetourDistanceRatio: parseFloat( + ALGORITHM: this.configService.get('ALGORITHM'), + STRICT: !!parseInt(this.configService.get('STRICT_ALGORITHM')), + REMOTENESS: parseInt(this.configService.get('REMOTENESS')), + USE_PROPORTION: !!parseInt(this.configService.get('USE_PROPORTION')), + PROPORTION: parseInt(this.configService.get('PROPORTION')), + USE_AZIMUTH: !!parseInt(this.configService.get('USE_AZIMUTH')), + AZIMUTH_MARGIN: parseInt(this.configService.get('AZIMUTH_MARGIN')), + MAX_DETOUR_DISTANCE_RATIO: parseFloat( this.configService.get('MAX_DETOUR_DISTANCE_RATIO'), ), - maxDetourDurationRatio: parseFloat( + MAX_DETOUR_DURATION_RATIO: parseFloat( this.configService.get('MAX_DETOUR_DURATION_RATIO'), ), - georouterType: this.configService.get('GEOROUTER_TYPE'), - georouterUrl: this.configService.get('GEOROUTER_URL'), + GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'), + GEOROUTER_URL: this.configService.get('GEOROUTER_URL'), }, }; }; diff --git a/src/modules/matcher/adapters/secondaries/geodesic.ts b/src/modules/matcher/adapters/secondaries/geodesic.ts index 3743ac6..deb304a 100644 --- a/src/modules/matcher/adapters/secondaries/geodesic.ts +++ b/src/modules/matcher/adapters/secondaries/geodesic.ts @@ -1,27 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; -import { Geodesic, GeodesicClass } from 'geographiclib-geodesic'; +import { Geodesic } from '../../../geography/adapters/secondaries/geodesic'; +import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface'; @Injectable() export class MatcherGeodesic implements IGeodesic { - private geod: GeodesicClass; - - constructor() { - this.geod = Geodesic.WGS84; - } + constructor(private readonly geodesic: Geodesic) {} inverse = ( lon1: number, lat1: number, lon2: number, lat2: number, - ): { azimuth: number; distance: number } => { - const { azi2: azimuth, s12: distance } = this.geod.Inverse( - lat1, - lon1, - lat2, - lon2, - ); - return { azimuth, distance }; - }; + ): { azimuth: number; distance: number } => + this.geodesic.inverse(lon1, lat1, lon2, lat2); } diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 33c79d8..472a333 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -5,9 +5,9 @@ import { Path } from '../../domain/types/path.type'; import { Injectable } from '@nestjs/common'; import { catchError, lastValueFrom, map } from 'rxjs'; import { AxiosError, AxiosResponse } from 'axios'; -import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; +import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface'; import { NamedRoute } from '../../domain/entities/ecosystem/named-route'; -import { Route } from '../../domain/entities/ecosystem/route'; +import { MatcherRoute } from '../../domain/entities/ecosystem/matcher-route'; import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point'; import { MatcherException, @@ -17,11 +17,11 @@ import { @Injectable() export class GraphhopperGeorouter implements IGeorouter { private url: string; - private urlArgs: Array; + private urlArgs: string[]; private withTime: boolean; private withPoints: boolean; private withDistance: boolean; - private paths: Array; + private paths: Path[]; private httpService: HttpService; private geodesic: IGeodesic; @@ -32,9 +32,9 @@ export class GraphhopperGeorouter implements IGeorouter { } route = async ( - paths: Array, + paths: Path[], settings: GeorouterSettings, - ): Promise> => { + ): Promise => { this.setDefaultUrlArgs(); this.setWithTime(settings.withTime); this.setWithPoints(settings.withPoints); @@ -70,7 +70,7 @@ export class GraphhopperGeorouter implements IGeorouter { } }; - private getRoutes = async (): Promise> => { + private getRoutes = async (): Promise => { const routes = Promise.all( this.paths.map(async (path) => { const url: string = [ @@ -106,8 +106,8 @@ export class GraphhopperGeorouter implements IGeorouter { private createRoute = ( response: AxiosResponse, - ): Route => { - const route = new Route(this.geodesic); + ): MatcherRoute => { + const route = new MatcherRoute(this.geodesic); if (response.data.paths && response.data.paths[0]) { const shortestPath = response.data.paths[0]; route.distance = shortestPath.distance ?? 0; @@ -125,7 +125,7 @@ export class GraphhopperGeorouter implements IGeorouter { shortestPath.snapped_waypoints && shortestPath.snapped_waypoints.coordinates ) { - let instructions: Array = []; + let instructions: GraphhopperInstruction[] = []; if (shortestPath.instructions) instructions = shortestPath.instructions; route.setSpacetimePoints( @@ -143,18 +143,18 @@ export class GraphhopperGeorouter implements IGeorouter { }; private generateSpacetimePoints = ( - points: Array>, - snappedWaypoints: Array>, - durations: Array>, - instructions: Array, - ): Array => { + points: Array, + snappedWaypoints: Array, + durations: Array, + instructions: GraphhopperInstruction[], + ): SpacetimePoint[] => { const indices = this.getIndices(points, snappedWaypoints); const times = this.getTimes(durations, indices); const distances = this.getDistances(instructions, indices); return indices.map( (index) => new SpacetimePoint( - points[index], + { lon: points[index][1], lat: points[index][0] }, times.find((time) => time.index == index)?.duration, distances.find((distance) => distance.index == index)?.distance, ), @@ -162,9 +162,9 @@ export class GraphhopperGeorouter implements IGeorouter { }; private getIndices = ( - points: Array>, - snappedWaypoints: Array>, - ): Array => { + points: Array, + snappedWaypoints: Array, + ): number[] => { const indices = snappedWaypoints.map((waypoint) => points.findIndex( (point) => point[0] == waypoint[0] && point[1] == waypoint[1], @@ -178,7 +178,7 @@ export class GraphhopperGeorouter implements IGeorouter { { index: number; originIndex: number; - waypoint: Array; + waypoint: number[]; nearest: number; distance: number; } @@ -212,8 +212,8 @@ export class GraphhopperGeorouter implements IGeorouter { }; private getTimes = ( - durations: Array>, - indices: Array, + durations: Array, + indices: number[], ): Array<{ index: number; duration: number }> => { const times: Array<{ index: number; duration: number }> = []; let duration = 0; @@ -262,8 +262,8 @@ export class GraphhopperGeorouter implements IGeorouter { }; private getDistances = ( - instructions: Array, - indices: Array, + instructions: GraphhopperInstruction[], + indices: number[], ): Array<{ index: number; distance: number }> => { let distance = 0; const distances: Array<{ index: number; distance: number }> = [ @@ -296,26 +296,26 @@ type GraphhopperResponse = { weight: number; time: number; points_encoded: boolean; - bbox: Array; + bbox: number[]; points: GraphhopperCoordinates; snapped_waypoints: GraphhopperCoordinates; details: { - time: Array>; + time: Array; }; - instructions: Array; + instructions: GraphhopperInstruction[]; }, ]; }; type GraphhopperCoordinates = { - coordinates: Array>; + coordinates: Array; }; type GraphhopperInstruction = { distance: number; heading: number; sign: GraphhopperSign; - interval: Array; + interval: number[]; text: string; }; diff --git a/src/modules/matcher/adapters/secondaries/time-converter.ts b/src/modules/matcher/adapters/secondaries/time-converter.ts new file mode 100644 index 0000000..63e8e62 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/time-converter.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { DateTime, TimeZone } from 'timezonecomplete'; +import { IConvertTime } from '../../domain/interfaces/time-converter.interface'; + +@Injectable() +export class TimeConverter implements IConvertTime { + toUtcDate = (date: Date, timezone: string): Date => { + try { + return new Date( + new DateTime( + `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`, + TimeZone.zone(timezone, false), + ) + .convert(TimeZone.zone('UTC')) + .toIsoString(), + ); + } catch (e) { + return undefined; + } + }; +} diff --git a/src/modules/matcher/adapters/secondaries/timezone-finder.ts b/src/modules/matcher/adapters/secondaries/timezone-finder.ts new file mode 100644 index 0000000..8459661 --- /dev/null +++ b/src/modules/matcher/adapters/secondaries/timezone-finder.ts @@ -0,0 +1,11 @@ +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); +} diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 4cb1ff2..bcd1824 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -10,27 +10,38 @@ import { Min, } from 'class-validator'; import { AutoMap } from '@automapper/classes'; -import { Point } from '../types/point.type'; +import { Point } from '../../../geography/domain/types/point.type'; import { Schedule } from '../types/schedule.type'; import { MarginDurations } from '../types/margin-durations.type'; import { AlgorithmType } from '../types/algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; -import { IRequestPerson } from '../interfaces/person-request.interface'; +import { IRequestAd } from '../interfaces/ad-request.interface'; import { IRequestGeography } from '../interfaces/geography-request.interface'; import { IRequestRequirement } from '../interfaces/requirement-request.interface'; import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface'; +import { Mode } from '../types/mode.enum'; export class MatchRequest implements IRequestTime, - IRequestPerson, + IRequestAd, IRequestGeography, IRequestRequirement, IRequestAlgorithmSettings { + @IsOptional() + @IsString() + @AutoMap() + uuid: string; + + @IsOptional() + @IsEnum(Mode) + @AutoMap() + mode: Mode; + @IsArray() @AutoMap() - waypoints: Array; + waypoints: Point[]; @IsOptional() @IsString() @@ -138,10 +149,7 @@ export class MatchRequest @IsOptional() @IsArray() - exclusions: Array; + exclusions: string[]; - @IsOptional() - @IsInt() - @AutoMap() - identifier: number; + timezone?: string; } diff --git a/src/modules/matcher/domain/entities/ecosystem/actor.ts b/src/modules/matcher/domain/entities/ecosystem/actor.ts index 25436e5..78ea643 100644 --- a/src/modules/matcher/domain/entities/ecosystem/actor.ts +++ b/src/modules/matcher/domain/entities/ecosystem/actor.ts @@ -1,14 +1,14 @@ import { Role } from '../../types/role.enum'; import { Step } from '../../types/step.enum'; -import { Person } from './person'; +import { Ad } from './ad'; export class Actor { - person: Person; + ad: Ad; role: Role; step: Step; - constructor(person: Person, role: Role, step: Step) { - this.person = person; + constructor(ad: Ad, role: Role, step: Step) { + this.ad = ad; this.role = role; this.step = step; } diff --git a/src/modules/matcher/domain/entities/ecosystem/ad.ts b/src/modules/matcher/domain/entities/ecosystem/ad.ts index 0350f1a..5046579 100644 --- a/src/modules/matcher/domain/entities/ecosystem/ad.ts +++ b/src/modules/matcher/domain/entities/ecosystem/ad.ts @@ -1,6 +1,40 @@ -import { AutoMap } from '@automapper/classes'; +import { IRequestAd } from '../../interfaces/ad-request.interface'; export class Ad { - @AutoMap() + private adRequest: IRequestAd; + private defaultUuid: string; + private defaultMarginDuration: number; uuid: string; + marginDurations: number[]; + + constructor( + adRequest: IRequestAd, + defaultUuid: string, + defaultMarginDuration: number, + ) { + this.adRequest = adRequest; + this.defaultUuid = defaultUuid; + this.defaultMarginDuration = defaultMarginDuration; + } + + init = (): void => { + this.setUuid(this.adRequest.uuid ?? this.defaultUuid); + this.setMarginDurations([ + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + ]); + }; + + setUuid = (uuid: string): void => { + this.uuid = uuid; + }; + + setMarginDurations = (marginDurations: number[]): void => { + this.marginDurations = marginDurations; + }; } diff --git a/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts index a6d4963..0e5dd92 100644 --- a/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts +++ b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts @@ -1,15 +1,15 @@ import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface'; import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type'; import { AlgorithmType } from '../../types/algorithm.enum'; -import { TimingFrequency } from '../../types/timing'; import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface'; import { IGeorouter } from '../../interfaces/georouter.interface'; +import { Frequency } from '../../../../ad/domain/types/frequency.enum'; export class AlgorithmSettings { private algorithmSettingsRequest: IRequestAlgorithmSettings; private strict: boolean; algorithmType: AlgorithmType; - restrict: TimingFrequency; + restrict: Frequency; remoteness: number; useProportion: boolean; proportion: number; @@ -22,38 +22,38 @@ export class AlgorithmSettings { constructor( algorithmSettingsRequest: IRequestAlgorithmSettings, defaultAlgorithmSettings: DefaultAlgorithmSettings, - frequency: TimingFrequency, + frequency: Frequency, georouterCreator: ICreateGeorouter, ) { this.algorithmSettingsRequest = algorithmSettingsRequest; this.algorithmType = - algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm; + algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.ALGORITHM; this.strict = - algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict; + algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.STRICT; this.remoteness = algorithmSettingsRequest.remoteness ? Math.abs(algorithmSettingsRequest.remoteness) - : defaultAlgorithmSettings.remoteness; + : defaultAlgorithmSettings.REMOTENESS; this.useProportion = algorithmSettingsRequest.useProportion ?? - defaultAlgorithmSettings.useProportion; + defaultAlgorithmSettings.USE_PROPORTION; this.proportion = algorithmSettingsRequest.proportion ? Math.abs(algorithmSettingsRequest.proportion) - : defaultAlgorithmSettings.proportion; + : defaultAlgorithmSettings.PROPORTION; this.useAzimuth = algorithmSettingsRequest.useAzimuth ?? - defaultAlgorithmSettings.useAzimuth; + defaultAlgorithmSettings.USE_AZIMUTH; this.azimuthMargin = algorithmSettingsRequest.azimuthMargin ? Math.abs(algorithmSettingsRequest.azimuthMargin) - : defaultAlgorithmSettings.azimuthMargin; + : defaultAlgorithmSettings.AZIMUTH_MARGIN; this.maxDetourDistanceRatio = algorithmSettingsRequest.maxDetourDistanceRatio ?? - defaultAlgorithmSettings.maxDetourDistanceRatio; + defaultAlgorithmSettings.MAX_DETOUR_DISTANCE_RATIO; this.maxDetourDurationRatio = algorithmSettingsRequest.maxDetourDurationRatio ?? - defaultAlgorithmSettings.maxDetourDurationRatio; + defaultAlgorithmSettings.MAX_DETOUR_DURATION_RATIO; this.georouter = georouterCreator.create( - defaultAlgorithmSettings.georouterType, - defaultAlgorithmSettings.georouterUrl, + defaultAlgorithmSettings.GEOROUTER_TYPE, + defaultAlgorithmSettings.GEOROUTER_URL, ); if (this.strict) { this.restrict = frequency; diff --git a/src/modules/matcher/domain/entities/ecosystem/geography.ts b/src/modules/matcher/domain/entities/ecosystem/geography.ts index 892e904..abf9be7 100644 --- a/src/modules/matcher/domain/entities/ecosystem/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -3,39 +3,42 @@ import { MatcherExceptionCode, } from '../../../exceptions/matcher.exception'; import { IRequestGeography } from '../../interfaces/geography-request.interface'; -import { PointType } from '../../types/geography.enum'; -import { Point } from '../../types/point.type'; -import { find } from 'geo-tz'; -import { Route } from './route'; +import { PointType } from '../../../../geography/domain/types/point-type.enum'; +import { Point } from '../../../../geography/domain/types/point.type'; +import { MatcherRoute } from './matcher-route'; import { Role } from '../../types/role.enum'; import { IGeorouter } from '../../interfaces/georouter.interface'; import { Waypoint } from './waypoint'; import { Actor } from './actor'; -import { Person } from './person'; +import { Ad } from './ad'; import { Step } from '../../types/step.enum'; import { Path } from '../../types/path.type'; +import { IFindTimezone } from '../../../../geography/domain/interfaces/timezone-finder.interface'; +import { Timezoner } from './timezoner'; export class Geography { private geographyRequest: IRequestGeography; - private person: Person; - private points: Array; + private ad: Ad; + private points: Point[]; originType: PointType; destinationType: PointType; - timezones: Array; - driverRoute: Route; - passengerRoute: Route; + timezones: string[]; + driverRoute: MatcherRoute; + passengerRoute: MatcherRoute; + timezoneFinder: IFindTimezone; constructor( geographyRequest: IRequestGeography, - defaultTimezone: string, - person: Person, + timezoner: Timezoner, + ad: Ad, ) { this.geographyRequest = geographyRequest; - this.person = person; + this.ad = ad; this.points = []; this.originType = undefined; this.destinationType = undefined; - this.timezones = [defaultTimezone]; + this.timezones = [timezoner.timezone]; + this.timezoneFinder = timezoner.finder; } init = (): void => { @@ -45,12 +48,12 @@ export class Geography { }; createRoutes = async ( - roles: Array, + roles: Role[], georouter: IGeorouter, ): Promise => { - let driverWaypoints: Array = []; - let passengerWaypoints: Array = []; - const paths: Array = []; + let driverWaypoints: Waypoint[] = []; + let passengerWaypoints: Waypoint[] = []; + const paths: Path[] = []; if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { if (this.points.length == 2) { // 2 points => same route for driver and passenger @@ -147,7 +150,7 @@ export class Geography { }; private setTimezones = (): void => { - this.timezones = find( + this.timezones = this.timezoneFinder.timezones( this.geographyRequest.waypoints[0].lat, this.geographyRequest.waypoints[0].lon, ); @@ -171,18 +174,15 @@ export class Geography { private isValidLatitude = (latitude: number): boolean => latitude >= -90 && latitude <= 90; - private createWaypoints = ( - points: Array, - role: Role, - ): Array => { + private createWaypoints = (points: Point[], role: Role): Waypoint[] => { return points.map((point, index) => { const waypoint = new Waypoint(point); if (index == 0) { - waypoint.addActor(new Actor(this.person, role, Step.START)); + waypoint.addActor(new Actor(this.ad, role, Step.START)); } else if (index == points.length - 1) { - waypoint.addActor(new Actor(this.person, role, Step.FINISH)); + waypoint.addActor(new Actor(this.ad, role, Step.FINISH)); } else { - waypoint.addActor(new Actor(this.person, role, Step.INTERMEDIATE)); + waypoint.addActor(new Actor(this.ad, role, Step.INTERMEDIATE)); } return waypoint; }); diff --git a/src/modules/matcher/domain/entities/ecosystem/matcher-route.ts b/src/modules/matcher/domain/entities/ecosystem/matcher-route.ts new file mode 100644 index 0000000..197741d --- /dev/null +++ b/src/modules/matcher/domain/entities/ecosystem/matcher-route.ts @@ -0,0 +1,16 @@ +import { Route } from '../../../../geography/domain/entities/route'; +import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface'; +import { Waypoint } from './waypoint'; + +export class MatcherRoute extends Route { + waypoints: Waypoint[]; + + constructor(geodesic: IGeodesic) { + super(geodesic); + } + + setWaypoints = (waypoints: Waypoint[]): void => { + this.waypoints = waypoints; + this.setAzimuth(waypoints.map((waypoint) => waypoint.point)); + }; +} diff --git a/src/modules/matcher/domain/entities/ecosystem/named-route.ts b/src/modules/matcher/domain/entities/ecosystem/named-route.ts index c57f928..c026769 100644 --- a/src/modules/matcher/domain/entities/ecosystem/named-route.ts +++ b/src/modules/matcher/domain/entities/ecosystem/named-route.ts @@ -1,6 +1,6 @@ -import { Route } from './route'; +import { MatcherRoute } from './matcher-route'; export type NamedRoute = { key: string; - route: Route; + route: MatcherRoute; }; diff --git a/src/modules/matcher/domain/entities/ecosystem/person.ts b/src/modules/matcher/domain/entities/ecosystem/person.ts deleted file mode 100644 index c6baa02..0000000 --- a/src/modules/matcher/domain/entities/ecosystem/person.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IRequestPerson } from '../../interfaces/person-request.interface'; - -export class Person { - private personRequest: IRequestPerson; - private defaultIdentifier: number; - private defaultMarginDuration: number; - identifier: number; - marginDurations: Array; - - constructor( - personRequest: IRequestPerson, - defaultIdentifier: number, - defaultMarginDuration: number, - ) { - this.personRequest = personRequest; - this.defaultIdentifier = defaultIdentifier; - this.defaultMarginDuration = defaultMarginDuration; - } - - init = (): void => { - this.setIdentifier(this.personRequest.identifier ?? this.defaultIdentifier); - this.setMarginDurations([ - this.defaultMarginDuration, - this.defaultMarginDuration, - this.defaultMarginDuration, - this.defaultMarginDuration, - this.defaultMarginDuration, - this.defaultMarginDuration, - this.defaultMarginDuration, - ]); - }; - - setIdentifier = (identifier: number): void => { - this.identifier = identifier; - }; - - setMarginDurations = (marginDurations: Array): void => { - this.marginDurations = marginDurations; - }; -} diff --git a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts index 98fe80f..8a45b80 100644 --- a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts +++ b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts @@ -1,10 +1,12 @@ +import { Coordinate } from '../../../../geography/domain/entities/coordinate'; + export class SpacetimePoint { - point: Array; + coordinate: Coordinate; duration: number; distance: number; - constructor(point: Array, duration: number, distance: number) { - this.point = point; + constructor(coordinate: Coordinate, duration: number, distance: number) { + this.coordinate = coordinate; this.duration = duration; this.distance = distance; } diff --git a/src/modules/matcher/domain/entities/ecosystem/time.ts b/src/modules/matcher/domain/entities/ecosystem/time.ts index 183a69a..83e3415 100644 --- a/src/modules/matcher/domain/entities/ecosystem/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -4,27 +4,31 @@ import { } from '../../../exceptions/matcher.exception'; import { MarginDurations } from '../../types/margin-durations.type'; import { IRequestTime } from '../../interfaces/time-request.interface'; -import { TimingDays, TimingFrequency, Days } from '../../types/timing'; -import { Schedule } from '../../types/schedule.type'; +import { DAYS } from '../../types/days.const'; +import { TimeSchedule } from '../../types/time-schedule.type'; +import { Frequency } from '../../../../ad/domain/types/frequency.enum'; +import { Day } from '../../types/day.type'; +import { IConvertTime } from '../../interfaces/time-converter.interface'; export class Time { private timeRequest: IRequestTime; - private defaultMarginDuration: number; private defaultValidityDuration: number; - frequency: TimingFrequency; + private timeConverter: IConvertTime; + frequency: Frequency; fromDate: Date; toDate: Date; - schedule: Schedule; + schedule: TimeSchedule; marginDurations: MarginDurations; constructor( timeRequest: IRequestTime, defaultMarginDuration: number, defaultValidityDuration: number, + timeConverter: IConvertTime, ) { this.timeRequest = timeRequest; - this.defaultMarginDuration = defaultMarginDuration; this.defaultValidityDuration = defaultValidityDuration; + this.timeConverter = timeConverter; this.schedule = {}; this.marginDurations = { mon: defaultMarginDuration, @@ -106,7 +110,7 @@ export class Time { } if ( !Object.keys(this.timeRequest.schedule).some((elem) => - Days.includes(elem), + DAYS.includes(elem), ) ) { throw new MatcherException( @@ -127,15 +131,17 @@ export class Time { private setPunctualRequest = (): void => { if (this.timeRequest.departure) { - this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; - this.schedule[TimingDays[this.fromDate.getDay()]] = - this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); + this.frequency = Frequency.PUNCTUAL; + this.schedule[Day[this.fromDate.getDay()]] = this.timeConverter.toUtcDate( + this.fromDate, + this.timeRequest.timezone, + ); } }; private setRecurrentRequest = (): void => { if (this.timeRequest.fromDate) { - this.frequency = TimingFrequency.FREQUENCY_RECURRENT; + this.frequency = Frequency.RECURRENT; if (!this.toDate) { this.toDate = this.addDays(this.fromDate, this.defaultValidityDuration); } @@ -145,7 +151,14 @@ export class Time { private setSchedule = (): void => { Object.keys(this.timeRequest.schedule).map((day) => { - this.schedule[day] = this.timeRequest.schedule[day]; + this.schedule[day] = this.timeConverter.toUtcDate( + new Date( + `${this.fromDate.getFullYear()}-${this.fromDate.getMonth()}-${this.fromDate.getDate()} ${ + this.timeRequest.schedule[day] + }`, + ), + this.timeRequest.timezone, + ); }); }; @@ -165,7 +178,7 @@ export class Time { if (this.timeRequest.marginDurations) { if ( !Object.keys(this.timeRequest.marginDurations).some((elem) => - Days.includes(elem), + DAYS.includes(elem), ) ) { throw new MatcherException( diff --git a/src/modules/matcher/domain/entities/ecosystem/timezoner.ts b/src/modules/matcher/domain/entities/ecosystem/timezoner.ts new file mode 100644 index 0000000..29f6e0b --- /dev/null +++ b/src/modules/matcher/domain/entities/ecosystem/timezoner.ts @@ -0,0 +1,6 @@ +import { IFindTimezone } from '../../../../geography/domain/interfaces/timezone-finder.interface'; + +export type Timezoner = { + timezone: string; + finder: IFindTimezone; +}; diff --git a/src/modules/matcher/domain/entities/ecosystem/waypoint.ts b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts index fdcbea0..48c0899 100644 --- a/src/modules/matcher/domain/entities/ecosystem/waypoint.ts +++ b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts @@ -1,9 +1,9 @@ -import { Point } from '../../types/point.type'; +import { Point } from '../../../../geography/domain/types/point.type'; import { Actor } from './actor'; export class Waypoint { point: Point; - actors: Array; + actors: Actor[]; constructor(point: Point) { this.point = point; diff --git a/src/modules/matcher/domain/entities/engine/candidate.ts b/src/modules/matcher/domain/entities/engine/candidate.ts index 1a19a59..0ace859 100644 --- a/src/modules/matcher/domain/entities/engine/candidate.ts +++ b/src/modules/matcher/domain/entities/engine/candidate.ts @@ -1,5 +1,5 @@ -import { Person } from '../ecosystem/person'; +import { Ad } from '../ecosystem/ad'; export class Candidate { - person: Person; + ad: Ad; } diff --git a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts index 0cc876d..9405266 100644 --- a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts @@ -5,7 +5,7 @@ import { Selector } from '../selector/selector.abstract'; export abstract class AlgorithmFactory { protected matchQuery: MatchQuery; - private candidates: Array; + private candidates: Candidate[]; constructor(matchQuery: MatchQuery) { this.matchQuery = matchQuery; @@ -13,5 +13,5 @@ export abstract class AlgorithmFactory { } abstract createSelector(): Selector; - abstract createProcessors(): Array; + abstract createProcessors(): Processor[]; } diff --git a/src/modules/matcher/domain/entities/engine/factory/classic.ts b/src/modules/matcher/domain/entities/engine/factory/classic.ts index 54880b0..fc06888 100644 --- a/src/modules/matcher/domain/entities/engine/factory/classic.ts +++ b/src/modules/matcher/domain/entities/engine/factory/classic.ts @@ -10,7 +10,7 @@ import { ClassicSelector } from '../selector/classic.selector'; export class ClassicAlgorithmFactory extends AlgorithmFactory { createSelector = (): Selector => new ClassicSelector(this.matchQuery); - createProcessors = (): Array => [ + createProcessors = (): Processor[] => [ new ClassicWaypointsCompleter(this.matchQuery), new RouteCompleter(this.matchQuery, true, true, true), new ClassicGeoFilter(this.matchQuery), diff --git a/src/modules/matcher/domain/entities/engine/matcher.ts b/src/modules/matcher/domain/entities/engine/matcher.ts index 48648af..923b69f 100644 --- a/src/modules/matcher/domain/entities/engine/matcher.ts +++ b/src/modules/matcher/domain/entities/engine/matcher.ts @@ -11,10 +11,10 @@ export class Matcher { private readonly algorithmFactoryCreator: AlgorithmFactoryCreator, ) {} - match = async (matchQuery: MatchQuery): Promise> => { + match = async (matchQuery: MatchQuery): Promise => { const algorithmFactory: AlgorithmFactory = this.algorithmFactoryCreator.create(matchQuery); - let candidates: Array = await algorithmFactory + let candidates: Candidate[] = await algorithmFactory .createSelector() .select(); for (const processor of algorithmFactory.createProcessors()) { diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts b/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts index baccba9..dee2a2f 100644 --- a/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts +++ b/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts @@ -2,7 +2,7 @@ import { Candidate } from '../../candidate'; import { Completer } from './completer.abstract'; export class ClassicWaypointsCompleter extends Completer { - complete = (candidates: Array): Array => { + complete = (candidates: Candidate[]): Candidate[] => { return candidates; }; } diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts b/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts index e11bfee..b72064f 100644 --- a/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts @@ -2,8 +2,7 @@ import { Candidate } from '../../candidate'; import { Processor } from '../processor.abstract'; export abstract class Completer extends Processor { - execute = (candidates: Array): Array => - this.complete(candidates); + execute = (candidates: Candidate[]): Candidate[] => this.complete(candidates); - abstract complete(candidates: Array): Array; + abstract complete(candidates: Candidate[]): Candidate[]; } diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/journey.completer.processor.ts b/src/modules/matcher/domain/entities/engine/processor/completer/journey.completer.processor.ts index 69042b9..d1a028c 100644 --- a/src/modules/matcher/domain/entities/engine/processor/completer/journey.completer.processor.ts +++ b/src/modules/matcher/domain/entities/engine/processor/completer/journey.completer.processor.ts @@ -2,7 +2,7 @@ import { Candidate } from '../../candidate'; import { Completer } from './completer.abstract'; export class JourneyCompleter extends Completer { - complete = (candidates: Array): Array => { + complete = (candidates: Candidate[]): Candidate[] => { return candidates; }; } diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/route.completer.processor.ts b/src/modules/matcher/domain/entities/engine/processor/completer/route.completer.processor.ts index 582bc03..38ca7b1 100644 --- a/src/modules/matcher/domain/entities/engine/processor/completer/route.completer.processor.ts +++ b/src/modules/matcher/domain/entities/engine/processor/completer/route.completer.processor.ts @@ -19,7 +19,7 @@ export class RouteCompleter extends Completer { this.withDistance = withDistance; } - complete = (candidates: Array): Array => { + complete = (candidates: Candidate[]): Candidate[] => { return candidates; }; } diff --git a/src/modules/matcher/domain/entities/engine/processor/filter/filter.abstract.ts b/src/modules/matcher/domain/entities/engine/processor/filter/filter.abstract.ts index 87cd490..1198383 100644 --- a/src/modules/matcher/domain/entities/engine/processor/filter/filter.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/processor/filter/filter.abstract.ts @@ -2,8 +2,7 @@ import { Candidate } from '../../candidate'; import { Processor } from '../processor.abstract'; export abstract class Filter extends Processor { - execute = (candidates: Array): Array => - this.filter(candidates); + execute = (candidates: Candidate[]): Candidate[] => this.filter(candidates); - abstract filter(candidates: Array): Array; + abstract filter(candidates: Candidate[]): Candidate[]; } diff --git a/src/modules/matcher/domain/entities/engine/processor/filter/geofilter/classic.filter.processor.ts b/src/modules/matcher/domain/entities/engine/processor/filter/geofilter/classic.filter.processor.ts index dc0dc66..77b4663 100644 --- a/src/modules/matcher/domain/entities/engine/processor/filter/geofilter/classic.filter.processor.ts +++ b/src/modules/matcher/domain/entities/engine/processor/filter/geofilter/classic.filter.processor.ts @@ -2,7 +2,7 @@ import { Candidate } from '../../../candidate'; import { Filter } from '../filter.abstract'; export class ClassicGeoFilter extends Filter { - filter = (candidates: Array): Array => { + filter = (candidates: Candidate[]): Candidate[] => { return candidates; }; } diff --git a/src/modules/matcher/domain/entities/engine/processor/filter/timefilter/classic.filter.processor.ts b/src/modules/matcher/domain/entities/engine/processor/filter/timefilter/classic.filter.processor.ts index b69c32e..2d48c49 100644 --- a/src/modules/matcher/domain/entities/engine/processor/filter/timefilter/classic.filter.processor.ts +++ b/src/modules/matcher/domain/entities/engine/processor/filter/timefilter/classic.filter.processor.ts @@ -2,7 +2,7 @@ import { Candidate } from '../../../candidate'; import { Filter } from '../filter.abstract'; export class ClassicTimeFilter extends Filter { - filter = (candidates: Array): Array => { + filter = (candidates: Candidate[]): Candidate[] => { return candidates; }; } diff --git a/src/modules/matcher/domain/entities/engine/processor/processor.abstract.ts b/src/modules/matcher/domain/entities/engine/processor/processor.abstract.ts index eee4c0c..d4eeabc 100644 --- a/src/modules/matcher/domain/entities/engine/processor/processor.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/processor/processor.abstract.ts @@ -8,5 +8,5 @@ export abstract class Processor { this.matchQuery = matchQuery; } - abstract execute(candidates: Array): Array; + abstract execute(candidates: Candidate[]): Candidate[]; } diff --git a/src/modules/matcher/domain/entities/engine/selector/classic.selector.ts b/src/modules/matcher/domain/entities/engine/selector/classic.selector.ts index e87403c..5b1d2da 100644 --- a/src/modules/matcher/domain/entities/engine/selector/classic.selector.ts +++ b/src/modules/matcher/domain/entities/engine/selector/classic.selector.ts @@ -2,7 +2,7 @@ import { Candidate } from '../candidate'; import { Selector } from './selector.abstract'; export class ClassicSelector extends Selector { - select = async (): Promise> => { + select = async (): Promise => { return []; }; } diff --git a/src/modules/matcher/domain/entities/engine/selector/selector.abstract.ts b/src/modules/matcher/domain/entities/engine/selector/selector.abstract.ts index b2b722e..c1b58bf 100644 --- a/src/modules/matcher/domain/entities/engine/selector/selector.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/selector/selector.abstract.ts @@ -8,5 +8,5 @@ export abstract class Selector { this.matchQuery = matchQuery; } - abstract select(): Promise>; + abstract select(): Promise; } diff --git a/src/modules/matcher/domain/interfaces/ad-request.interface.ts b/src/modules/matcher/domain/interfaces/ad-request.interface.ts new file mode 100644 index 0000000..4914482 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/ad-request.interface.ts @@ -0,0 +1,3 @@ +export interface IRequestAd { + uuid?: string; +} diff --git a/src/modules/matcher/domain/interfaces/geography-request.interface.ts b/src/modules/matcher/domain/interfaces/geography-request.interface.ts index d10a6ac..8a58ac1 100644 --- a/src/modules/matcher/domain/interfaces/geography-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/geography-request.interface.ts @@ -1,5 +1,5 @@ -import { Point } from '../types/point.type'; +import { Point } from '../../../geography/domain/types/point.type'; export interface IRequestGeography { - waypoints: Array; + waypoints: Point[]; } diff --git a/src/modules/matcher/domain/interfaces/georouter.interface.ts b/src/modules/matcher/domain/interfaces/georouter.interface.ts index 5f09b23..7c64cc2 100644 --- a/src/modules/matcher/domain/interfaces/georouter.interface.ts +++ b/src/modules/matcher/domain/interfaces/georouter.interface.ts @@ -3,8 +3,5 @@ import { GeorouterSettings } from '../types/georouter-settings.type'; import { Path } from '../types/path.type'; export interface IGeorouter { - route( - paths: Array, - settings: GeorouterSettings, - ): Promise>; + route(paths: Path[], settings: GeorouterSettings): Promise; } diff --git a/src/modules/matcher/domain/interfaces/person-request.interface.ts b/src/modules/matcher/domain/interfaces/person-request.interface.ts deleted file mode 100644 index 9dd8075..0000000 --- a/src/modules/matcher/domain/interfaces/person-request.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IRequestPerson { - identifier?: number; -} diff --git a/src/modules/matcher/domain/interfaces/time-converter.interface.ts b/src/modules/matcher/domain/interfaces/time-converter.interface.ts new file mode 100644 index 0000000..cbbbfb0 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/time-converter.interface.ts @@ -0,0 +1,3 @@ +export interface IConvertTime { + toUtcDate(date: Date, timezone: string): Date; +} diff --git a/src/modules/matcher/domain/interfaces/time-request.interface.ts b/src/modules/matcher/domain/interfaces/time-request.interface.ts index 1f8c6a7..d7f7df7 100644 --- a/src/modules/matcher/domain/interfaces/time-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/time-request.interface.ts @@ -8,4 +8,5 @@ export interface IRequestTime { schedule?: Schedule; marginDuration?: number; marginDurations?: MarginDurations; + timezone?: string; } diff --git a/src/modules/matcher/domain/types/actor.type..ts b/src/modules/matcher/domain/types/actor.type..ts index aecaa9e..22315f7 100644 --- a/src/modules/matcher/domain/types/actor.type..ts +++ b/src/modules/matcher/domain/types/actor.type..ts @@ -1,9 +1,9 @@ -import { Person } from '../entities/ecosystem/person'; +import { Ad } from '../entities/ecosystem/ad'; import { Role } from './role.enum'; import { Step } from './step.enum'; export type Actor = { - person: Person; + ad: Ad; role: Role; step: Step; }; diff --git a/src/modules/matcher/domain/types/day.type.ts b/src/modules/matcher/domain/types/day.type.ts new file mode 100644 index 0000000..c275d7a --- /dev/null +++ b/src/modules/matcher/domain/types/day.type.ts @@ -0,0 +1,9 @@ +export enum Day { + 'sun', + 'mon', + 'tue', + 'wed', + 'thu', + 'fri', + 'sat', +} diff --git a/src/modules/matcher/domain/types/days.const.ts b/src/modules/matcher/domain/types/days.const.ts new file mode 100644 index 0000000..4794839 --- /dev/null +++ b/src/modules/matcher/domain/types/days.const.ts @@ -0,0 +1 @@ +export const DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; diff --git a/src/modules/matcher/domain/types/default-algorithm-settings.type.ts b/src/modules/matcher/domain/types/default-algorithm-settings.type.ts index a9edb47..98fa3b1 100644 --- a/src/modules/matcher/domain/types/default-algorithm-settings.type.ts +++ b/src/modules/matcher/domain/types/default-algorithm-settings.type.ts @@ -1,15 +1,15 @@ import { AlgorithmType } from './algorithm.enum'; export type DefaultAlgorithmSettings = { - algorithm: AlgorithmType; - strict: boolean; - remoteness: number; - useProportion: boolean; - proportion: number; - useAzimuth: boolean; - azimuthMargin: number; - maxDetourDistanceRatio: number; - maxDetourDurationRatio: number; - georouterType: string; - georouterUrl: string; + ALGORITHM: AlgorithmType; + STRICT: boolean; + REMOTENESS: number; + USE_PROPORTION: boolean; + PROPORTION: number; + USE_AZIMUTH: boolean; + AZIMUTH_MARGIN: number; + MAX_DETOUR_DISTANCE_RATIO: number; + MAX_DETOUR_DURATION_RATIO: number; + GEOROUTER_TYPE: string; + GEOROUTER_URL: string; }; diff --git a/src/modules/matcher/domain/types/default-params.type.ts b/src/modules/matcher/domain/types/default-params.type.ts index f39bd3b..c91a26a 100644 --- a/src/modules/matcher/domain/types/default-params.type.ts +++ b/src/modules/matcher/domain/types/default-params.type.ts @@ -1,7 +1,7 @@ import { DefaultAlgorithmSettings } from './default-algorithm-settings.type'; export type IDefaultParams = { - DEFAULT_IDENTIFIER: number; + DEFAULT_UUID: string; MARGIN_DURATION: number; VALIDITY_DURATION: number; DEFAULT_TIMEZONE: string; diff --git a/src/modules/matcher/domain/types/mode.enum.ts b/src/modules/matcher/domain/types/mode.enum.ts new file mode 100644 index 0000000..be6d1eb --- /dev/null +++ b/src/modules/matcher/domain/types/mode.enum.ts @@ -0,0 +1,5 @@ +export enum Mode { + MATCH = 'MATCH', + PUBLISH = 'PUBLISH', + PUBLISH_AND_MATCH = 'PUBLISH_AND_MATCH', +} diff --git a/src/modules/matcher/domain/types/path.type.ts b/src/modules/matcher/domain/types/path.type.ts index 8a1bfe9..44e03b6 100644 --- a/src/modules/matcher/domain/types/path.type.ts +++ b/src/modules/matcher/domain/types/path.type.ts @@ -1,6 +1,6 @@ -import { Point } from './point.type'; +import { Point } from '../../../geography/domain/types/point.type'; export type Path = { key: string; - points: Array; + points: Point[]; }; diff --git a/src/modules/matcher/domain/types/point.type.ts b/src/modules/matcher/domain/types/point.type.ts deleted file mode 100644 index 8d32fe0..0000000 --- a/src/modules/matcher/domain/types/point.type.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PointType } from './geography.enum'; - -export type Point = { - lon: number; - lat: number; - type?: PointType; -}; diff --git a/src/modules/matcher/domain/types/time-schedule.type.ts b/src/modules/matcher/domain/types/time-schedule.type.ts new file mode 100644 index 0000000..4bd6ea2 --- /dev/null +++ b/src/modules/matcher/domain/types/time-schedule.type.ts @@ -0,0 +1,9 @@ +export type TimeSchedule = { + mon?: Date; + tue?: Date; + wed?: Date; + thu?: Date; + fri?: Date; + sat?: Date; + sun?: Date; +}; diff --git a/src/modules/matcher/domain/types/timing.ts b/src/modules/matcher/domain/types/timing.ts deleted file mode 100644 index 567595a..0000000 --- a/src/modules/matcher/domain/types/timing.ts +++ /dev/null @@ -1,16 +0,0 @@ -export enum TimingFrequency { - FREQUENCY_PUNCTUAL = 1, - FREQUENCY_RECURRENT = 2, -} - -export enum TimingDays { - 'sun', - 'mon', - 'tue', - 'wed', - 'thu', - 'fri', - 'sat', -} - -export const Days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; diff --git a/src/modules/matcher/domain/types/waypoint.ts b/src/modules/matcher/domain/types/waypoint.ts index 6ee5941..bc15ea5 100644 --- a/src/modules/matcher/domain/types/waypoint.ts +++ b/src/modules/matcher/domain/types/waypoint.ts @@ -1,7 +1,7 @@ import { Actor } from './actor.type.'; -import { Point } from './point.type'; +import { Point } from '../../../geography/domain/types/point.type'; export type Waypoint = { point: Point; - actors: Array; + actors: Actor[]; }; diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index fbb6952..8af7355 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -4,28 +4,28 @@ import { QueryHandler } from '@nestjs/cqrs'; import { Messager } from '../../adapters/secondaries/messager'; import { MatchQuery } from '../../queries/match.query'; import { Match } from '../entities/ecosystem/match'; -import { ICollection } from '../../../database/src/interfaces/collection.interface'; +import { ICollection } from '../../../database/interfaces/collection.interface'; import { Matcher } from '../entities/engine/matcher'; @QueryHandler(MatchQuery) export class MatchUseCase { constructor( - private readonly _matcher: Matcher, - private readonly _messager: Messager, - @InjectMapper() private readonly _mapper: Mapper, + private readonly matcher: Matcher, + private readonly messager: Messager, + @InjectMapper() private readonly mapper: Mapper, ) {} execute = async (matchQuery: MatchQuery): Promise> => { try { - const data: Array = await this._matcher.match(matchQuery); - this._messager.publish('matcher.match', 'match !'); + const data: Match[] = await this.matcher.match(matchQuery); + this.messager.publish('matcher.match', 'match !'); return { data, total: data.length, }; } catch (error) { const err: Error = error; - this._messager.publish( + this.messager.publish( 'logging.matcher.match.crit', JSON.stringify({ matchQuery, diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index aacb6b2..fe84110 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -5,7 +5,6 @@ import { CqrsModule } from '@nestjs/cqrs'; import { DatabaseModule } from '../database/database.module'; import { MatcherController } from './adapters/primaries/matcher.controller'; import { MatchProfile } from './mappers/match.profile'; -import { AdRepository } from './adapters/secondaries/ad.repository'; import { MatchUseCase } from './domain/usecases/match.usecase'; import { Messager } from './adapters/secondaries/messager'; import { CacheModule } from '@nestjs/cache-manager'; @@ -17,9 +16,14 @@ import { HttpModule } from '@nestjs/axios'; import { MatcherGeodesic } from './adapters/secondaries/geodesic'; import { Matcher } from './domain/entities/engine/matcher'; import { AlgorithmFactoryCreator } from './domain/entities/engine/factory/algorithm-factory-creator'; +import { TimezoneFinder } from './adapters/secondaries/timezone-finder'; +import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder'; +import { GeographyModule } from '../geography/geography.module'; +import { TimeConverter } from './adapters/secondaries/time-converter'; @Module({ imports: [ + GeographyModule, DatabaseModule, CqrsModule, HttpModule, @@ -53,14 +57,16 @@ import { AlgorithmFactoryCreator } from './domain/entities/engine/factory/algori controllers: [MatcherController], providers: [ MatchProfile, - AdRepository, Messager, DefaultParamsProvider, MatchUseCase, GeorouterCreator, MatcherGeodesic, + TimezoneFinder, + TimeConverter, Matcher, AlgorithmFactoryCreator, + GeoTimezoneFinder, ], exports: [], }) diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index b5c62fc..8ac0388 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -1,6 +1,6 @@ import { MatchRequest } from '../domain/dtos/match.request'; import { Geography } from '../domain/entities/ecosystem/geography'; -import { Person } from '../domain/entities/ecosystem/person'; +import { Ad } from '../domain/entities/ecosystem/ad'; import { Requirement } from '../domain/entities/ecosystem/requirement'; import { Role } from '../domain/types/role.enum'; import { AlgorithmSettings } from '../domain/entities/ecosystem/algorithm-settings'; @@ -8,96 +8,116 @@ import { Time } from '../domain/entities/ecosystem/time'; import { IDefaultParams } from '../domain/types/default-params.type'; import { IGeorouter } from '../domain/interfaces/georouter.interface'; import { ICreateGeorouter } from '../domain/interfaces/georouter-creator.interface'; +import { IFindTimezone } from '../../geography/domain/interfaces/timezone-finder.interface'; +import { Mode } from '../domain/types/mode.enum'; +import { IConvertTime } from '../domain/interfaces/time-converter.interface'; export class MatchQuery { - private readonly _matchRequest: MatchRequest; - private readonly _defaultParams: IDefaultParams; - private readonly _georouterCreator: ICreateGeorouter; - person: Person; - roles: Array; + private readonly matchRequest: MatchRequest; + private readonly defaultParams: IDefaultParams; + private readonly georouterCreator: ICreateGeorouter; + mode: Mode; + ad: Ad; + roles: Role[]; time: Time; geography: Geography; - exclusions: Array; + exclusions: string[]; requirement: Requirement; algorithmSettings: AlgorithmSettings; georouter: IGeorouter; + timezoneFinder: IFindTimezone; + timeConverter: IConvertTime; constructor( matchRequest: MatchRequest, defaultParams: IDefaultParams, georouterCreator: ICreateGeorouter, + timezoneFinder: IFindTimezone, + timeConverter: IConvertTime, ) { - this._matchRequest = matchRequest; - this._defaultParams = defaultParams; - this._georouterCreator = georouterCreator; - this._setPerson(); - this._setRoles(); - this._setTime(); - this._setGeography(); - this._setRequirement(); - this._setAlgorithmSettings(); - this._setExclusions(); + this.matchRequest = matchRequest; + this.defaultParams = defaultParams; + this.georouterCreator = georouterCreator; + this.timezoneFinder = timezoneFinder; + this.timeConverter = timeConverter; + this.setMode(); + this.setAd(); + this.setRoles(); + this.setGeography(); + this.setTime(); + this.setRequirement(); + this.setAlgorithmSettings(); + this.setExclusions(); } createRoutes = (): void => { this.geography.createRoutes(this.roles, this.algorithmSettings.georouter); }; - _setPerson = (): void => { - this.person = new Person( - this._matchRequest, - this._defaultParams.DEFAULT_IDENTIFIER, - this._defaultParams.MARGIN_DURATION, - ); - this.person.init(); + private setMode = (): void => { + this.mode = this.matchRequest.mode ?? Mode.MATCH; }; - _setRoles = (): void => { + private setAd = (): void => { + this.ad = new Ad( + this.matchRequest, + this.defaultParams.DEFAULT_UUID, + this.defaultParams.MARGIN_DURATION, + ); + this.ad.init(); + }; + + private setRoles = (): void => { this.roles = []; - if (this._matchRequest.driver) this.roles.push(Role.DRIVER); - if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER); + if (this.matchRequest.driver) this.roles.push(Role.DRIVER); + if (this.matchRequest.passenger) this.roles.push(Role.PASSENGER); if (this.roles.length == 0) this.roles.push(Role.PASSENGER); }; - _setTime = (): void => { + private setGeography = (): void => { + this.geography = new Geography( + this.matchRequest, + { + timezone: this.defaultParams.DEFAULT_TIMEZONE, + finder: this.timezoneFinder, + }, + this.ad, + ); + this.geography.init(); + if (this.geography.timezones.length > 0) + this.matchRequest.timezone = this.geography.timezones[0]; + }; + + private setTime = (): void => { this.time = new Time( - this._matchRequest, - this._defaultParams.MARGIN_DURATION, - this._defaultParams.VALIDITY_DURATION, + this.matchRequest, + this.defaultParams.MARGIN_DURATION, + this.defaultParams.VALIDITY_DURATION, + this.timeConverter, ); this.time.init(); }; - _setGeography = (): void => { - this.geography = new Geography( - this._matchRequest, - this._defaultParams.DEFAULT_TIMEZONE, - this.person, - ); - this.geography.init(); - }; - - _setRequirement = (): void => { + private setRequirement = (): void => { this.requirement = new Requirement( - this._matchRequest, - this._defaultParams.DEFAULT_SEATS, + this.matchRequest, + this.defaultParams.DEFAULT_SEATS, ); }; - _setAlgorithmSettings = (): void => { + private setAlgorithmSettings = (): void => { this.algorithmSettings = new AlgorithmSettings( - this._matchRequest, - this._defaultParams.DEFAULT_ALGORITHM_SETTINGS, + this.matchRequest, + this.defaultParams.DEFAULT_ALGORITHM_SETTINGS, this.time.frequency, - this._georouterCreator, + this.georouterCreator, ); }; - _setExclusions = (): void => { + private setExclusions = (): void => { this.exclusions = []; - if (this._matchRequest.identifier) - this.exclusions.push(this._matchRequest.identifier); - if (this._matchRequest.exclusions) - this.exclusions.push(...this._matchRequest.exclusions); + if (this.matchRequest.uuid) this.exclusions.push(this.matchRequest.uuid); + if (this.matchRequest.exclusions) + this.exclusions.push(...this.matchRequest.exclusions); }; } diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts index 5221c14..a23a4d0 100644 --- a/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/default-params.provider.spec.ts @@ -33,6 +33,6 @@ describe('DefaultParamsProvider', () => { it('should provide default params', async () => { const params: IDefaultParams = defaultParamsProvider.getParams(); - expect(params.DEFAULT_IDENTIFIER).toBe(99); + expect(params.DEFAULT_UUID).toBe(99); }); }); diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts index 9e08335..6e878a9 100644 --- a/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/geodesic.spec.ts @@ -1,14 +1,38 @@ +import { Test, TestingModule } from '@nestjs/testing'; import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic'; +import { Geodesic } from '../../../../../geography/adapters/secondaries/geodesic'; + +const mockGeodesic = { + inverse: jest.fn().mockImplementation(() => ({ + azimuth: 45, + distance: 50000, + })), +}; describe('Matcher geodesic', () => { + let matcherGeodesic: MatcherGeodesic; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + MatcherGeodesic, + { + provide: Geodesic, + useValue: mockGeodesic, + }, + ], + }).compile(); + + matcherGeodesic = module.get(MatcherGeodesic); + }); + it('should be defined', () => { - const geodesic: MatcherGeodesic = new MatcherGeodesic(); - expect(geodesic).toBeDefined(); + expect(matcherGeodesic).toBeDefined(); }); it('should get inverse values', () => { - const geodesic: MatcherGeodesic = new MatcherGeodesic(); - const inv = geodesic.inverse(0, 0, 1, 1); + const inv = matcherGeodesic.inverse(0, 0, 1, 1); expect(Math.round(inv.azimuth)).toBe(45); - expect(Math.round(inv.distance)).toBe(156900); + expect(Math.round(inv.distance)).toBe(50000); }); }); diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/timezone-finder.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/timezone-finder.spec.ts new file mode 100644 index 0000000..63d8462 --- /dev/null +++ b/src/modules/matcher/tests/unit/adapters/secondaries/timezone-finder.spec.ts @@ -0,0 +1,35 @@ +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); + }); + + 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'); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts index 9f93de6..3480b2b 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -1,19 +1,19 @@ -import { Person } from '../../../../domain/entities/ecosystem/person'; +import { Ad } from '../../../../domain/entities/ecosystem/ad'; import { Geography, RouteKey, } from '../../../../domain/entities/ecosystem/geography'; import { Role } from '../../../../domain/types/role.enum'; import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route'; -import { Route } from '../../../../domain/entities/ecosystem/route'; -import { IGeodesic } from '../../../../domain/interfaces/geodesic.interface'; -import { PointType } from '../../../../domain/types/geography.enum'; +import { MatcherRoute } from '../../../../domain/entities/ecosystem/matcher-route'; +import { IGeodesic } from '../../../../../geography/domain/interfaces/geodesic.interface'; +import { PointType } from '../../../../../geography/domain/types/point-type.enum'; -const person: Person = new Person( +const ad: Ad = new Ad( { - identifier: 1, + uuid: '774aaab2-77df-4c6c-b70d-7b9e972e5bbc', }, - 0, + '00000000-0000-0000-0000-000000000000', 900, ); @@ -31,7 +31,7 @@ const mockGeorouter = { return [ { key: RouteKey.COMMON, - route: new Route(mockGeodesic), + route: new MatcherRoute(mockGeodesic), }, ]; }) @@ -39,11 +39,11 @@ const mockGeorouter = { return [ { key: RouteKey.DRIVER, - route: new Route(mockGeodesic), + route: new MatcherRoute(mockGeodesic), }, { key: RouteKey.PASSENGER, - route: new Route(mockGeodesic), + route: new MatcherRoute(mockGeodesic), }, ]; }) @@ -51,7 +51,7 @@ const mockGeorouter = { return [ { key: RouteKey.DRIVER, - route: new Route(mockGeodesic), + route: new MatcherRoute(mockGeodesic), }, ]; }) @@ -59,12 +59,16 @@ const mockGeorouter = { return [ { key: RouteKey.PASSENGER, - route: new Route(mockGeodesic), + route: new MatcherRoute(mockGeodesic), }, ]; }), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + describe('Geography entity', () => { it('should be defined', () => { const geography = new Geography( @@ -80,8 +84,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); expect(geography).toBeDefined(); }); @@ -103,8 +110,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); geography.init(); expect(geography.originType).toBe(PointType.LOCALITY); @@ -115,8 +125,11 @@ describe('Geography entity', () => { { waypoints: [], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -130,8 +143,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -149,8 +165,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -168,8 +187,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -190,8 +212,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); geography.init(); await geography.createRoutes( @@ -220,8 +245,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); geography.init(); await geography.createRoutes( @@ -246,8 +274,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); geography.init(); await geography.createRoutes([Role.DRIVER], mockGeorouter); @@ -269,8 +300,11 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', - person, + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, + ad, ); geography.init(); await geography.createRoutes([Role.PASSENGER], mockGeorouter); diff --git a/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/matcher-route.spec.ts similarity index 76% rename from src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts rename to src/modules/matcher/tests/unit/domain/ecosystem/matcher-route.spec.ts index 16d27a3..feb1b6e 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/matcher-route.spec.ts @@ -1,4 +1,4 @@ -import { Route } from '../../../../domain/entities/ecosystem/route'; +import { MatcherRoute } from '../../../../domain/entities/ecosystem/matcher-route'; import { SpacetimePoint } from '../../../../domain/entities/ecosystem/spacetime-point'; import { Waypoint } from '../../../../domain/entities/ecosystem/waypoint'; @@ -17,13 +17,13 @@ const mockGeodesic = { }), }; -describe('Route entity', () => { +describe('Matcher route entity', () => { it('should be defined', () => { - const route = new Route(mockGeodesic); + const route = new MatcherRoute(mockGeodesic); expect(route).toBeDefined(); }); it('should set waypoints and geodesic values for a route', () => { - const route = new Route(mockGeodesic); + const route = new MatcherRoute(mockGeodesic); const waypoint1: Waypoint = new Waypoint({ lon: 0, lat: 0, @@ -39,7 +39,7 @@ describe('Route entity', () => { expect(route.distanceAzimuth).toBe(50000); }); it('should set points and geodesic values for a route', () => { - const route = new Route(mockGeodesic); + const route = new MatcherRoute(mockGeodesic); route.setPoints([ { lon: 10, @@ -56,9 +56,9 @@ describe('Route entity', () => { expect(route.distanceAzimuth).toBe(60000); }); it('should set spacetimePoints for a route', () => { - const route = new Route(mockGeodesic); - const spacetimePoint1 = new SpacetimePoint([0, 0], 0, 0); - const spacetimePoint2 = new SpacetimePoint([10, 10], 500, 5000); + const route = new MatcherRoute(mockGeodesic); + const spacetimePoint1 = new SpacetimePoint({ lon: 0, lat: 0 }, 0, 0); + const spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000); route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]); expect(route.spacetimePoints.length).toBe(2); }); diff --git a/src/modules/matcher/tests/unit/domain/ecosystem/person.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/person.spec.ts index c9d604c..974d93e 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/person.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/person.spec.ts @@ -1,40 +1,40 @@ -import { Person } from '../../../../domain/entities/ecosystem/person'; +import { Ad } from '../../../../domain/entities/ecosystem/ad'; -const DEFAULT_IDENTIFIER = 0; +const DEFAULT_UUID = '00000000-0000-0000-0000-000000000000'; const MARGIN_DURATION = 900; -describe('Person entity', () => { +describe('Ad entity', () => { it('should be defined', () => { - const person = new Person( + const ad = new Ad( { - identifier: 1, + uuid: '774aaab2-77df-4c6c-b70d-7b9e972e5bbc', }, - DEFAULT_IDENTIFIER, + DEFAULT_UUID, MARGIN_DURATION, ); - expect(person).toBeDefined(); + expect(ad).toBeDefined(); }); describe('init', () => { - it('should initialize a person with an identifier', () => { - const person = new Person( + it('should initialize an ad with a uuid', () => { + const ad = new Ad( { - identifier: 1, + uuid: '774aaab2-77df-4c6c-b70d-7b9e972e5bbc', }, - DEFAULT_IDENTIFIER, + DEFAULT_UUID, MARGIN_DURATION, ); - person.init(); - expect(person.identifier).toBe(1); - expect(person.marginDurations[0]).toBe(900); - expect(person.marginDurations[6]).toBe(900); + ad.init(); + expect(ad.uuid).toBe('774aaab2-77df-4c6c-b70d-7b9e972e5bbc'); + expect(ad.marginDurations[0]).toBe(900); + expect(ad.marginDurations[6]).toBe(900); }); - it('should initialize a person without an identifier', () => { - const person = new Person({}, DEFAULT_IDENTIFIER, MARGIN_DURATION); - person.init(); - expect(person.identifier).toBe(0); - expect(person.marginDurations[0]).toBe(900); - expect(person.marginDurations[6]).toBe(900); + it('should initialize an ad without a uuid', () => { + const ad = new Ad({}, DEFAULT_UUID, MARGIN_DURATION); + ad.init(); + expect(ad.uuid).toBe('00000000-0000-0000-0000-000000000000'); + expect(ad.marginDurations[0]).toBe(900); + expect(ad.marginDurations[6]).toBe(900); }); }); }); diff --git a/src/modules/matcher/tests/unit/domain/ecosystem/time.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/time.spec.ts index fa5772e..1df151d 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/time.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/time.spec.ts @@ -2,6 +2,9 @@ import { Time } from '../../../../domain/entities/ecosystem/time'; const MARGIN_DURATION = 900; const VALIDITY_DURATION = 365; +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; describe('Time entity', () => { it('should be defined', () => { @@ -11,6 +14,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(time).toBeDefined(); }); @@ -23,6 +27,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); time.init(); expect(time.fromDate.getFullYear()).toBe( @@ -37,6 +42,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); time.init(); expect(time.marginDurations['tue']).toBe(300); @@ -51,6 +57,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); time.init(); expect(time.marginDurations['tue']).toBe(900); @@ -67,6 +74,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); time.init(); expect(time.marginDurations['tue']).toBe(500); @@ -82,6 +90,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); time.init(); expect(time.fromDate.getFullYear()).toBe( @@ -89,7 +98,12 @@ describe('Time entity', () => { ); }); it('should throw an exception if no date is provided', () => { - const time = new Time({}, MARGIN_DURATION, VALIDITY_DURATION); + const time = new Time( + {}, + MARGIN_DURATION, + VALIDITY_DURATION, + mockTimeConverter, + ); expect(() => time.init()).toThrow(); }); it('should throw an exception if punctual date is invalid', () => { @@ -99,6 +113,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); @@ -109,6 +124,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); @@ -120,6 +136,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); @@ -131,6 +148,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); @@ -142,6 +160,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); @@ -154,6 +173,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); @@ -168,6 +188,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); @@ -180,6 +201,7 @@ describe('Time entity', () => { }, MARGIN_DURATION, VALIDITY_DURATION, + mockTimeConverter, ); expect(() => time.init()).toThrow(); }); diff --git a/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts index 79bf1fc..7c715a6 100644 --- a/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('AlgorithmFactoryCreator', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/algorithm-factory.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory.abstract.spec.ts index 7ec1886..74e2494 100644 --- a/src/modules/matcher/tests/unit/domain/engine/algorithm-factory.abstract.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory.abstract.spec.ts @@ -11,24 +11,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -48,6 +56,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); class FakeSelector extends Selector { diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-algorithm-factory.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-algorithm-factory.spec.ts index 45d8a31..4859189 100644 --- a/src/modules/matcher/tests/unit/domain/engine/classic-algorithm-factory.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/classic-algorithm-factory.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('ClassicAlgorithmFactory', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-geo.filter.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-geo.filter.processor.spec.ts index abd06b8..ea3b506 100644 --- a/src/modules/matcher/tests/unit/domain/engine/classic-geo.filter.processor.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/classic-geo.filter.processor.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('ClassicGeoFilter', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-time.filter.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-time.filter.processor.spec.ts index fe92e70..c489684 100644 --- a/src/modules/matcher/tests/unit/domain/engine/classic-time.filter.processor.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/classic-time.filter.processor.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('ClassicTimeFilter', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-waypoint.completer.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-waypoint.completer.processor.spec.ts index 500193e..68cfca7 100644 --- a/src/modules/matcher/tests/unit/domain/engine/classic-waypoint.completer.processor.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/classic-waypoint.completer.processor.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('ClassicWaypointCompleter', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/classic.selector.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic.selector.spec.ts index 8eb2954..19dcfdb 100644 --- a/src/modules/matcher/tests/unit/domain/engine/classic.selector.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/classic.selector.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('ClassicSelector', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/completer.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/completer.abstract.spec.ts index f94bbcf..7d4bdb9 100644 --- a/src/modules/matcher/tests/unit/domain/engine/completer.abstract.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/completer.abstract.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); class FakeCompleter extends Completer { diff --git a/src/modules/matcher/tests/unit/domain/engine/filter.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/filter.abstract.spec.ts index dfb1e64..3e6ceb6 100644 --- a/src/modules/matcher/tests/unit/domain/engine/filter.abstract.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/filter.abstract.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); class FakeFilter extends Filter { diff --git a/src/modules/matcher/tests/unit/domain/engine/journey.completer.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/journey.completer.processor.spec.ts index 9eb9a58..85bd6d8 100644 --- a/src/modules/matcher/tests/unit/domain/engine/journey.completer.processor.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/journey.completer.processor.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('JourneyCompleter', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts index 0f1aca7..5b73254 100644 --- a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts @@ -21,24 +21,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -58,6 +66,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('Matcher', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/processor.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/processor.abstract.spec.ts index 66bfefb..c83eb82 100644 --- a/src/modules/matcher/tests/unit/domain/engine/processor.abstract.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/processor.abstract.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); class FakeProcessor extends Processor { diff --git a/src/modules/matcher/tests/unit/domain/engine/route.completer.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/route.completer.processor.spec.ts index 4863945..cdd2a5a 100644 --- a/src/modules/matcher/tests/unit/domain/engine/route.completer.processor.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/route.completer.processor.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); describe('RouteCompleter', () => { diff --git a/src/modules/matcher/tests/unit/domain/engine/selector.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/selector.abstract.spec.ts index 01f9eb7..677e43f 100644 --- a/src/modules/matcher/tests/unit/domain/engine/selector.abstract.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/selector.abstract.spec.ts @@ -9,24 +9,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -46,6 +54,8 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); class FakeSelector extends Selector { diff --git a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts index c5e0fda..8ed9acf 100644 --- a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -34,24 +34,32 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -97,7 +105,13 @@ describe('MatchUseCase', () => { describe('execute', () => { it('should return matches', async () => { const matches = await matchUseCase.execute( - new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), + new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, + ), ); expect(matches.total).toBe(3); }); @@ -105,7 +119,13 @@ describe('MatchUseCase', () => { it('should throw an exception when error occurs', async () => { await expect( matchUseCase.execute( - new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), + new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, + ), ), ).rejects.toBeInstanceOf(MatcherException); }); diff --git a/src/modules/matcher/tests/unit/queries/match.query.spec.ts b/src/modules/matcher/tests/unit/queries/match.query.spec.ts index f761bcf..b11a0e5 100644 --- a/src/modules/matcher/tests/unit/queries/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -1,28 +1,29 @@ import { MatchRequest } from '../../../domain/dtos/match.request'; import { Role } from '../../../domain/types/role.enum'; -import { TimingFrequency } from '../../../domain/types/timing'; import { IDefaultParams } from '../../../domain/types/default-params.type'; import { MatchQuery } from '../../../queries/match.query'; import { AlgorithmType } from '../../../domain/types/algorithm.enum'; +import { Frequency } from '../../../../ad/domain/types/frequency.enum'; +import { Mode } from '../../../domain/types/mode.enum'; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: AlgorithmType.CLASSIC, - strict: false, - remoteness: 15000, - useProportion: true, - proportion: 0.3, - useAzimuth: true, - azimuthMargin: 10, - maxDetourDistanceRatio: 0.3, - maxDetourDurationRatio: 0.3, - georouterType: 'graphhopper', - georouterUrl: 'http://localhost', + ALGORITHM: AlgorithmType.CLASSIC, + STRICT: false, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', }, }; @@ -30,6 +31,14 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), +}; + describe('Match query', () => { it('should be defined', () => { const matchRequest: MatchRequest = new MatchRequest(); @@ -48,8 +57,35 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery).toBeDefined(); + expect(matchQuery.mode).toBe(Mode.MATCH); + }); + + it('should create a query with publish and match mode', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.mode = Mode.PUBLISH_AND_MATCH; + const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, + ); + expect(matchQuery.mode).toBe(Mode.PUBLISH_AND_MATCH); }); it('should create a query with excluded identifiers', () => { @@ -65,12 +101,18 @@ describe('Match query', () => { lon: 3.045432, }, ]; - matchRequest.identifier = 125; - matchRequest.exclusions = [126, 127, 128]; + matchRequest.uuid = '445aa6e4-99e4-4899-9456-3be8c3ada368'; + matchRequest.exclusions = [ + 'eacf5e53-e63c-4551-860c-73f95b8a8895', + 'a4098161-13a9-4e55-8999-de134fbf89c4', + 'b18f7ffa-20b9-4a1a-89bc-e238ea8289f3', + ]; const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.exclusions.length).toBe(4); }); @@ -93,6 +135,8 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.roles).toEqual([Role.DRIVER]); }); @@ -115,6 +159,8 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.roles).toEqual([Role.PASSENGER]); }); @@ -138,6 +184,8 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.roles.length).toBe(2); expect(matchQuery.roles).toContain(Role.PASSENGER); @@ -163,6 +211,8 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.requirement.seatsDriver).toBe(1); expect(matchQuery.requirement.seatsPassenger).toBe(2); @@ -194,13 +244,13 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.algorithmSettings.algorithmType).toBe( AlgorithmType.CLASSIC, ); - expect(matchQuery.algorithmSettings.restrict).toBe( - TimingFrequency.FREQUENCY_PUNCTUAL, - ); + expect(matchQuery.algorithmSettings.restrict).toBe(Frequency.PUNCTUAL); expect(matchQuery.algorithmSettings.useProportion).toBeTruthy(); expect(matchQuery.algorithmSettings.proportion).toBe(0.45); expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy(); diff --git a/src/modules/utils/exception-code.enum.ts b/src/modules/utils/exception-code.enum.ts new file mode 100644 index 0000000..59554ae --- /dev/null +++ b/src/modules/utils/exception-code.enum.ts @@ -0,0 +1,19 @@ +export enum ExceptionCode { + OK = 0, + CANCELLED = 1, + UNKNOWN = 2, + INVALID_ARGUMENT = 3, + DEADLINE_EXCEEDED = 4, + NOT_FOUND = 5, + ALREADY_EXISTS = 6, + PERMISSION_DENIED = 7, + RESOURCE_EXHAUSTED = 8, + FAILED_PRECONDITION = 9, + ABORTED = 10, + OUT_OF_RANGE = 11, + UNIMPLEMENTED = 12, + INTERNAL = 13, + UNAVAILABLE = 14, + DATA_LOSS = 15, + UNAUTHENTICATED = 16, +}