From e07e19261ea808dbc6202b9c4c97317511244bbe Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 24 Apr 2023 16:44:52 +0200 Subject: [PATCH 01/19] wip --- src/app.module.ts | 2 + src/modules/ad/ad.modules.ts | 34 +++++++++++++++ .../primaries/ad-messager.controller.ts | 19 +++++++++ src/modules/ad/domain/entities/ad.ts | 41 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 src/modules/ad/ad.modules.ts create mode 100644 src/modules/ad/adapters/primaries/ad-messager.controller.ts create mode 100644 src/modules/ad/domain/entities/ad.ts diff --git a/src/app.module.ts b/src/app.module.ts index 69bca0e..8236fec 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.modules'; @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.modules.ts b/src/modules/ad/ad.modules.ts new file mode 100644 index 0000000..bb8b761 --- /dev/null +++ b/src/modules/ad/ad.modules.ts @@ -0,0 +1,34 @@ +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'; + +@Module({ + imports: [ + 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', + }, + }, + uri: configService.get('RMQ_URI'), + connectionInitOptions: { wait: true }, + enableControllerDiscovery: true, + }), + inject: [ConfigService], + }), + ], + controllers: [AdMessagerController], + providers: [], + 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..6d8e1ab --- /dev/null +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -0,0 +1,19 @@ +import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; +import { Controller } from '@nestjs/common'; +// import { Ad } from '../../domain/entities/ad'; + +@Controller() +export class AdMessagerController { + @RabbitSubscribe({ + name: 'adCreated', + }) + public adCreatedHandler(message: string) { + console.log(JSON.parse(message)); + // try { + // const createdAd: Ad = JSON.parse(message); + // console.log(createdAd); + // } catch (e) { + // console.log('error', e); + // } + } +} diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts new file mode 100644 index 0000000..b4725ad --- /dev/null +++ b/src/modules/ad/domain/entities/ad.ts @@ -0,0 +1,41 @@ +import { AutoMap } from '@automapper/classes'; + +export class Ad { + @AutoMap() + uuid: string; + + driver: boolean; + passenger: boolean; + frequency: number; + 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; + driverDistance: number; + passengerDuration: number; + passengerDistance: number; + originType: number; + destinationType: number; + waypoints: []; + direction: string; + fwdAzimuth: number; + backAzimuth: number; + seatsDriver: number; + seatsPassenger: number; + seatsUsed: number; + createdAt: string; + updatedAt: string; +} From ca693087d28e69a81972c5ac7a2859cf7f884857 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 24 Apr 2023 16:55:14 +0200 Subject: [PATCH 02/19] update broker handler --- src/modules/ad/ad.modules.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/ad/ad.modules.ts b/src/modules/ad/ad.modules.ts index bb8b761..7b98c89 100644 --- a/src/modules/ad/ad.modules.ts +++ b/src/modules/ad/ad.modules.ts @@ -18,10 +18,13 @@ import { AdMessagerController } from './adapters/primaries/ad-messager.controlle adCreated: { exchange: configService.get('RMQ_EXCHANGE'), routingKey: 'ad.created', + queue: `${configService.get( + 'RMQ_EXCHANGE', + )}-matcher-ad-created`, }, }, uri: configService.get('RMQ_URI'), - connectionInitOptions: { wait: true }, + connectionInitOptions: { wait: false }, enableControllerDiscovery: true, }), inject: [ConfigService], From aeead7fb62c5031a4789f659fabb3bdb4b809175 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 25 Apr 2023 17:49:47 +0200 Subject: [PATCH 03/19] create ad WIP, extract geography methods to dedicated module --- .../20230406093419_init/migration.sql | 65 ---------- .../migrations/20230425130853_/migration.sql | 65 ++++++++++ prisma/schema.prisma | 76 ++++++------ src/modules/ad/ad.modules.ts | 13 +- .../primaries/ad-messager.controller.ts | 31 +++-- .../adapters/primaries/ad.presenter.ts} | 2 +- .../ad/adapters/secondaries/ad.repository.ts | 79 ++++++++++++ src/modules/ad/commands/create-ad.command.ts | 9 ++ .../ad/domain/dtos/create-ad.request.ts | 116 ++++++++++++++++++ src/modules/ad/domain/entities/ad.ts | 67 ++++++++++ .../ad/domain/usecases/create-ad.usecase.ts | 17 +++ src/modules/ad/mappers/ad.profile.ts | 18 +++ src/modules/database/database.module.ts | 6 +- .../secondaries/prisma-repository.abstract.ts | 8 +- .../secondaries/geo-timezone-finder.ts | 8 ++ .../adapters/secondaries/geodesic.ts | 27 ++++ .../domain/interfaces/geodesic.interface.ts | 0 .../interfaces/timezone-finder.interface.ts | 3 + src/modules/geography/geography.module.ts | 9 ++ .../tests/unit/geo-timezone-finder.spec.ts | 14 +++ .../geography/tests/unit/geodesic.spec.ts | 14 +++ .../prisma.health-indicator.usecase.ts | 2 +- src/modules/health/health.module.ts | 2 +- .../prisma.health-indicator.usecase.spec.ts | 2 +- .../adapters/primaries/matcher.controller.ts | 15 ++- .../adapters/secondaries/ad.repository.ts | 8 -- .../matcher/adapters/secondaries/geodesic.ts | 21 +--- .../secondaries/graphhopper-georouter.ts | 2 +- .../adapters/secondaries/timezone-finder.ts | 11 ++ .../domain/entities/ecosystem/geography.ts | 11 +- .../domain/entities/ecosystem/route.ts | 2 +- .../domain/entities/ecosystem/timezoner.ts | 6 + src/modules/matcher/matcher.module.ts | 8 +- src/modules/matcher/queries/match.query.ts | 85 +++++++------ .../secondaries/geo-timezone-finder.spec.ts | 35 ++++++ .../adapters/secondaries/geodesic.spec.ts | 34 ++++- .../unit/domain/ecosystem/geography.spec.ts | 56 +++++++-- .../engine/algorithm-factory-creator.spec.ts | 5 + .../engine/algorithm-factory.abstract.spec.ts | 5 + .../engine/classic-algorithm-factory.spec.ts | 5 + .../classic-geo.filter.processor.spec.ts | 5 + .../classic-time.filter.processor.spec.ts | 5 + ...assic-waypoint.completer.processor.spec.ts | 5 + .../domain/engine/classic.selector.spec.ts | 5 + .../domain/engine/completer.abstract.spec.ts | 5 + .../domain/engine/filter.abstract.spec.ts | 5 + .../journey.completer.processor.spec.ts | 5 + .../tests/unit/domain/engine/matcher.spec.ts | 5 + .../domain/engine/processor.abstract.spec.ts | 5 + .../engine/route.completer.processor.spec.ts | 5 + .../domain/engine/selector.abstract.spec.ts | 5 + .../tests/unit/domain/match.usecase.spec.ts | 18 ++- .../tests/unit/queries/match.query.spec.ts | 11 ++ 53 files changed, 824 insertions(+), 222 deletions(-) delete mode 100644 prisma/migrations/20230406093419_init/migration.sql create mode 100644 prisma/migrations/20230425130853_/migration.sql rename src/modules/{matcher/domain/entities/ecosystem/ad.ts => ad/adapters/primaries/ad.presenter.ts} (74%) create mode 100644 src/modules/ad/adapters/secondaries/ad.repository.ts create mode 100644 src/modules/ad/commands/create-ad.command.ts create mode 100644 src/modules/ad/domain/dtos/create-ad.request.ts create mode 100644 src/modules/ad/domain/usecases/create-ad.usecase.ts create mode 100644 src/modules/ad/mappers/ad.profile.ts create mode 100644 src/modules/geography/adapters/secondaries/geo-timezone-finder.ts create mode 100644 src/modules/geography/adapters/secondaries/geodesic.ts rename src/modules/{matcher => geography}/domain/interfaces/geodesic.interface.ts (100%) create mode 100644 src/modules/geography/domain/interfaces/timezone-finder.interface.ts create mode 100644 src/modules/geography/geography.module.ts create mode 100644 src/modules/geography/tests/unit/geo-timezone-finder.spec.ts create mode 100644 src/modules/geography/tests/unit/geodesic.spec.ts delete mode 100644 src/modules/matcher/adapters/secondaries/ad.repository.ts create mode 100644 src/modules/matcher/adapters/secondaries/timezone-finder.ts create mode 100644 src/modules/matcher/domain/entities/ecosystem/timezoner.ts create mode 100644 src/modules/matcher/tests/unit/adapters/secondaries/geo-timezone-finder.spec.ts 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/20230425130853_/migration.sql b/prisma/migrations/20230425130853_/migration.sql new file mode 100644 index 0000000..8c7b5e6 --- /dev/null +++ b/prisma/migrations/20230425130853_/migration.sql @@ -0,0 +1,65 @@ +-- 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, + "fromDate" DATE NOT NULL, + "toDate" DATE NOT NULL, + "monTime" TIMESTAMPTZ NOT NULL, + "tueTime" TIMESTAMPTZ NOT NULL, + "wedTime" TIMESTAMPTZ NOT NULL, + "thuTime" TIMESTAMPTZ NOT NULL, + "friTime" TIMESTAMPTZ NOT NULL, + "satTime" TIMESTAMPTZ NOT NULL, + "sunTime" TIMESTAMPTZ NOT NULL, + "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 NOT NULL, + "driverDistance" INTEGER NOT NULL, + "passengerDuration" INTEGER NOT NULL, + "passengerDistance" INTEGER NOT NULL, + "originType" SMALLINT NOT NULL, + "destinationType" SMALLINT NOT NULL, + "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, + "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..469a604 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,47 +13,47 @@ 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 @default(uuid()) @db.Uuid + driver Boolean + passenger Boolean + frequency Int + 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 + originType Int @db.SmallInt + destinationType Int @db.SmallInt + waypoints Unsupported("geography(LINESTRING)")? + direction Unsupported("geography(LINESTRING)")? + fwdAzimuth Int + backAzimuth Int + seatsDriver Int @db.SmallInt + seatsPassenger Int @db.SmallInt + seatsUsed Int @db.SmallInt + 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") } diff --git a/src/modules/ad/ad.modules.ts b/src/modules/ad/ad.modules.ts index 7b98c89..e59f7fd 100644 --- a/src/modules/ad/ad.modules.ts +++ b/src/modules/ad/ad.modules.ts @@ -2,9 +2,16 @@ 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'; @Module({ imports: [ + DatabaseModule, + CqrsModule, RabbitMQModule.forRootAsync(RabbitMQModule, { imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ @@ -18,9 +25,7 @@ import { AdMessagerController } from './adapters/primaries/ad-messager.controlle adCreated: { exchange: configService.get('RMQ_EXCHANGE'), routingKey: 'ad.created', - queue: `${configService.get( - 'RMQ_EXCHANGE', - )}-matcher-ad-created`, + queue: 'matcher-ad-created', }, }, uri: configService.get('RMQ_URI'), @@ -31,7 +36,7 @@ import { AdMessagerController } from './adapters/primaries/ad-messager.controlle }), ], controllers: [AdMessagerController], - providers: [], + providers: [AdProfile, 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 index 6d8e1ab..5a09dd4 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -1,19 +1,32 @@ import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; import { Controller } from '@nestjs/common'; -// import { Ad } from '../../domain/entities/ad'; +import { Ad } from '../../domain/entities/ad'; +import { InjectMapper } from '@automapper/nestjs'; +import { Mapper } from '@automapper/core'; +import { CommandBus } from '@nestjs/cqrs'; +import { CreateAdCommand } from '../../commands/create-ad.command'; +import { AdPresenter } from './ad.presenter'; +import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; @Controller() export class AdMessagerController { + constructor( + private readonly _commandBus: CommandBus, + @InjectMapper() private readonly _mapper: Mapper, + ) {} + @RabbitSubscribe({ name: 'adCreated', }) - public adCreatedHandler(message: string) { - console.log(JSON.parse(message)); - // try { - // const createdAd: Ad = JSON.parse(message); - // console.log(createdAd); - // } catch (e) { - // console.log('error', e); - // } + async adCreatedHandler(message: string): Promise { + try { + const createAdRequest: CreateAdRequest = JSON.parse(message); + const ad: Ad = await this._commandBus.execute( + new CreateAdCommand(createAdRequest), + ); + return this._mapper.map(ad, Ad, AdPresenter); + } catch (e) { + console.log('error', e); + } } } diff --git a/src/modules/matcher/domain/entities/ecosystem/ad.ts b/src/modules/ad/adapters/primaries/ad.presenter.ts similarity index 74% rename from src/modules/matcher/domain/entities/ecosystem/ad.ts rename to src/modules/ad/adapters/primaries/ad.presenter.ts index 0350f1a..1a4d67c 100644 --- a/src/modules/matcher/domain/entities/ecosystem/ad.ts +++ b/src/modules/ad/adapters/primaries/ad.presenter.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; -export class Ad { +export class AdPresenter { @AutoMap() uuid: string; } 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..b100584 --- /dev/null +++ b/src/modules/ad/adapters/secondaries/ad.repository.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { MatcherRepository } from '../../../database/src/domain/matcher-repository'; +import { Ad } from '../../domain/entities/ad'; +import { DatabaseException } from '../../../database/src/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}'`, + driver: ad.driver ? 'true' : 'false', + passenger: ad.passenger ? 'true' : 'false', + frequency: ad.frequency, + fromDate: `'${ad.fromDate}'`, + toDate: `'${ad.toDate}'`, + monTime: `'${ad.monTime}'`, + tueTime: `'${ad.tueTime}'`, + wedTime: `'${ad.wedTime}'`, + thuTime: `'${ad.thuTime}'`, + friTime: `'${ad.friTime}'`, + satTime: `'${ad.satTime}'`, + sunTime: `'${ad.sunTime}'`, + }; + } +} + +type AdFields = { + uuid: string; + driver: string; + passenger: string; + frequency: number; + 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; + driverDistance: number; + passengerDuration: number; + passengerDistance: number; + originType: number; + destinationType: number; + waypoints: string; + direction: string; + fwdAzimuth: number; + backAzimuth: number; + seatsDriver: number; + seatsPassenger: number; + seatsUsed: number; + createdAt: string; + updatedAt: string; +}; 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..1b0e230 --- /dev/null +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -0,0 +1,116 @@ +import { AutoMap } from '@automapper/classes'; +import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; + +export class CreateAdRequest { + @IsString() + @IsNotEmpty() + @AutoMap() + uuid: string; + + @IsBoolean() + @AutoMap() + driver: boolean; + + @IsBoolean() + @AutoMap() + passenger: boolean; + + @IsNumber() + @AutoMap() + frequency: number; + + @IsString() + @AutoMap() + fromDate: string; + + @IsString() + @AutoMap() + toDate: string; + + @IsString() + @AutoMap() + monTime: string; + + @IsString() + @AutoMap() + tueTime: string; + + @IsString() + @AutoMap() + wedTime: string; + + @IsString() + @AutoMap() + thuTime: string; + + @IsString() + @AutoMap() + friTime: string; + + @IsString() + @AutoMap() + satTime: string; + + @IsString() + @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; + + @IsNumber() + @AutoMap() + originType: number; + + @IsNumber() + @AutoMap() + destinationType: number; + + @AutoMap() + waypoints: []; + + @IsNumber() + @AutoMap() + seatsDriver: number; + + @IsNumber() + @AutoMap() + seatsPassenger: number; + + @IsNumber() + @AutoMap() + seatsUsed: number; + + @IsString() + @AutoMap() + createdAt: string; + + @IsString() + @AutoMap() + updatedAt: string; +} diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index b4725ad..e8998ec 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -4,38 +4,105 @@ export class Ad { @AutoMap() uuid: string; + @AutoMap() driver: boolean; + + @AutoMap() passenger: boolean; + + @AutoMap() frequency: number; + + @AutoMap() fromDate: string; + + @AutoMap() toDate: string; + + @AutoMap() monTime: string; + + @AutoMap() tueTime: string; + + @AutoMap() wedTime: string; + + @AutoMap() thuTime: string; + + @AutoMap() friTime: string; + + @AutoMap() satTime: string; + + @AutoMap() sunTime: string; + + @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() originType: number; + + @AutoMap() destinationType: number; + + @AutoMap() waypoints: []; + + @AutoMap() direction: string; + + @AutoMap() fwdAzimuth: number; + + @AutoMap() backAzimuth: number; + + @AutoMap() seatsDriver: number; + + @AutoMap() seatsPassenger: number; + + @AutoMap() seatsUsed: number; + + @AutoMap() createdAt: string; + + @AutoMap() updatedAt: string; } 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..b9b3b9e --- /dev/null +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -0,0 +1,17 @@ +import { CommandHandler } from '@nestjs/cqrs'; +import { CreateAdCommand } from '../../commands/create-ad.command'; +import { Ad } from '../entities/ad'; +import { AdRepository } from '../../adapters/secondaries/ad.repository'; + +@CommandHandler(CreateAdCommand) +export class CreateAdUseCase { + constructor(private readonly adRepository: AdRepository) {} + + async execute(command: CreateAdCommand): Promise { + try { + return await this.adRepository.createAd(command.createAdRequest); + } catch (error) { + throw error; + } + } +} diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts new file mode 100644 index 0000000..289adbc --- /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 { AdPresenter } from '../adapters/primaries/ad.presenter'; + +@Injectable() +export class AdProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper: any) => { + createMap(mapper, Ad, AdPresenter); + }; + } +} diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index 61328fa..2d18636 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 { 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/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts index fa2ba59..e93f896 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -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,7 +219,7 @@ 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 { 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..1ca02c2 --- /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): Array => 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/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/timezone-finder.interface.ts b/src/modules/geography/domain/interfaces/timezone-finder.interface.ts new file mode 100644 index 0000000..6f22169 --- /dev/null +++ b/src/modules/geography/domain/interfaces/timezone-finder.interface.ts @@ -0,0 +1,3 @@ +export interface IFindTimezone { + timezones(lon: number, lat: number): Array; +} 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/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/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..22b3b4a 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -11,6 +11,7 @@ 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'; @UsePipes( new RpcValidationPipe({ @@ -21,20 +22,22 @@ import { Match } from '../../domain/entities/ecosystem/match'; @Controller() export class MatcherController { constructor( - private readonly _queryBus: QueryBus, - private readonly _defaultParamsProvider: DefaultParamsProvider, + private readonly queryBus: QueryBus, + private readonly defaultParamsProvider: DefaultParamsProvider, @InjectMapper() private readonly _mapper: Mapper, - private readonly _georouterCreator: GeorouterCreator, + private readonly georouterCreator: GeorouterCreator, + private readonly timezoneFinder: GeoTimezoneFinder, ) {} @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, ), ); return Promise.resolve({ 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/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..9d85e85 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -5,7 +5,7 @@ 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 { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point'; 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..5ce3306 --- /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): Array => + this.geoTimezoneFinder.timezones(lon, lat); +} diff --git a/src/modules/matcher/domain/entities/ecosystem/geography.ts b/src/modules/matcher/domain/entities/ecosystem/geography.ts index 892e904..56a08a5 100644 --- a/src/modules/matcher/domain/entities/ecosystem/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -5,7 +5,6 @@ import { 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 { Role } from '../../types/role.enum'; import { IGeorouter } from '../../interfaces/georouter.interface'; @@ -14,6 +13,8 @@ import { Actor } from './actor'; import { Person } from './person'; 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; @@ -24,10 +25,11 @@ export class Geography { timezones: Array; driverRoute: Route; passengerRoute: Route; + timezoneFinder: IFindTimezone; constructor( geographyRequest: IRequestGeography, - defaultTimezone: string, + timezoner: Timezoner, person: Person, ) { this.geographyRequest = geographyRequest; @@ -35,7 +37,8 @@ export class Geography { this.points = []; this.originType = undefined; this.destinationType = undefined; - this.timezones = [defaultTimezone]; + this.timezones = [timezoner.timezone]; + this.timezoneFinder = timezoner.finder; } init = (): void => { @@ -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, ); diff --git a/src/modules/matcher/domain/entities/ecosystem/route.ts b/src/modules/matcher/domain/entities/ecosystem/route.ts index d2b1238..2bb27b8 100644 --- a/src/modules/matcher/domain/entities/ecosystem/route.ts +++ b/src/modules/matcher/domain/entities/ecosystem/route.ts @@ -1,4 +1,4 @@ -import { IGeodesic } from '../../interfaces/geodesic.interface'; +import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface'; import { Point } from '../../types/point.type'; import { SpacetimePoint } from './spacetime-point'; import { Waypoint } from './waypoint'; 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/matcher.module.ts b/src/modules/matcher/matcher.module.ts index aacb6b2..39e1dfc 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,13 @@ 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'; @Module({ imports: [ + GeographyModule, DatabaseModule, CqrsModule, HttpModule, @@ -53,14 +56,15 @@ import { AlgorithmFactoryCreator } from './domain/entities/engine/factory/algori controllers: [MatcherController], providers: [ MatchProfile, - AdRepository, Messager, DefaultParamsProvider, MatchUseCase, GeorouterCreator, MatcherGeodesic, + TimezoneFinder, Matcher, AlgorithmFactoryCreator, + GeoTimezoneFinder, ], exports: [], }) diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index b5c62fc..3f7c8dd 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -8,11 +8,12 @@ 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'; export class MatchQuery { - private readonly _matchRequest: MatchRequest; - private readonly _defaultParams: IDefaultParams; - private readonly _georouterCreator: ICreateGeorouter; + private readonly matchRequest: MatchRequest; + private readonly defaultParams: IDefaultParams; + private readonly georouterCreator: ICreateGeorouter; person: Person; roles: Array; time: Time; @@ -21,83 +22,89 @@ export class MatchQuery { requirement: Requirement; algorithmSettings: AlgorithmSettings; georouter: IGeorouter; + timezoneFinder: IFindTimezone; constructor( matchRequest: MatchRequest, defaultParams: IDefaultParams, georouterCreator: ICreateGeorouter, + timezoneFinder: IFindTimezone, ) { - 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.setPerson(); + this.setRoles(); + this.setTime(); + this.setGeography(); + this.setRequirement(); + this.setAlgorithmSettings(); + this.setExclusions(); } createRoutes = (): void => { this.geography.createRoutes(this.roles, this.algorithmSettings.georouter); }; - _setPerson = (): void => { + private setPerson = (): void => { this.person = new Person( - this._matchRequest, - this._defaultParams.DEFAULT_IDENTIFIER, - this._defaultParams.MARGIN_DURATION, + this.matchRequest, + this.defaultParams.DEFAULT_IDENTIFIER, + this.defaultParams.MARGIN_DURATION, ); this.person.init(); }; - _setRoles = (): void => { + 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 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.time.init(); }; - _setGeography = (): void => { + private setGeography = (): void => { this.geography = new Geography( - this._matchRequest, - this._defaultParams.DEFAULT_TIMEZONE, + this.matchRequest, + { + timezone: this.defaultParams.DEFAULT_TIMEZONE, + finder: this.timezoneFinder, + }, 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.identifier) + this.exclusions.push(this.matchRequest.identifier); + if (this.matchRequest.exclusions) + this.exclusions.push(...this.matchRequest.exclusions); }; } diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/geo-timezone-finder.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/geo-timezone-finder.spec.ts new file mode 100644 index 0000000..5dccb46 --- /dev/null +++ b/src/modules/matcher/tests/unit/adapters/secondaries/geo-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('Geo TZ 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/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/domain/ecosystem/geography.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts index 9f93de6..2503dcc 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -6,7 +6,7 @@ import { 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 { IGeodesic } from '../../../../../geography/domain/interfaces/geodesic.interface'; import { PointType } from '../../../../domain/types/geography.enum'; const person: Person = new Person( @@ -65,6 +65,10 @@ const mockGeorouter = { }), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + describe('Geography entity', () => { it('should be defined', () => { const geography = new Geography( @@ -80,7 +84,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); expect(geography).toBeDefined(); @@ -103,7 +110,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); geography.init(); @@ -115,7 +125,10 @@ describe('Geography entity', () => { { waypoints: [], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); expect(() => geography.init()).toThrow(); @@ -130,7 +143,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); expect(() => geography.init()).toThrow(); @@ -149,7 +165,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); expect(() => geography.init()).toThrow(); @@ -168,7 +187,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); expect(() => geography.init()).toThrow(); @@ -190,7 +212,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); geography.init(); @@ -220,7 +245,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); geography.init(); @@ -246,7 +274,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); geography.init(); @@ -269,7 +300,10 @@ describe('Geography entity', () => { }, ], }, - 'Europe/Paris', + { + timezone: 'Europe/Paris', + finder: mockTimezoneFinder, + }, person, ); geography.init(); 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..1a816ee 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..f17f5a4 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,6 +11,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -48,6 +52,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..690fee3 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..fda1087 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..6c703af 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..9a52ce4 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..77d616b 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..15fad37 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..6b96578 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..eb79edf 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..1240682 100644 --- a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts @@ -21,6 +21,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -58,6 +62,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..65f6a00 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..7076539 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..d8830cb 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,6 +9,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -46,6 +50,7 @@ const matchQuery: MatchQuery = new MatchQuery( matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); 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..1ca097b 100644 --- a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -34,6 +34,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, MARGIN_DURATION: 900, @@ -97,7 +101,12 @@ 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, + ), ); expect(matches.total).toBe(3); }); @@ -105,7 +114,12 @@ 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, + ), ), ).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..92dae34 100644 --- a/src/modules/matcher/tests/unit/queries/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -30,6 +30,10 @@ const mockGeorouterCreator = { create: jest.fn().mockImplementation(), }; +const mockTimezoneFinder = { + timezones: jest.fn().mockImplementation(), +}; + describe('Match query', () => { it('should be defined', () => { const matchRequest: MatchRequest = new MatchRequest(); @@ -48,6 +52,7 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); expect(matchQuery).toBeDefined(); }); @@ -71,6 +76,7 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); expect(matchQuery.exclusions.length).toBe(4); }); @@ -93,6 +99,7 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); expect(matchQuery.roles).toEqual([Role.DRIVER]); }); @@ -115,6 +122,7 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); expect(matchQuery.roles).toEqual([Role.PASSENGER]); }); @@ -138,6 +146,7 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); expect(matchQuery.roles.length).toBe(2); expect(matchQuery.roles).toContain(Role.PASSENGER); @@ -163,6 +172,7 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); expect(matchQuery.requirement.seatsDriver).toBe(1); expect(matchQuery.requirement.seatsPassenger).toBe(2); @@ -194,6 +204,7 @@ describe('Match query', () => { matchRequest, defaultParams, mockGeorouterCreator, + mockTimezoneFinder, ); expect(matchQuery.algorithmSettings.algorithmType).toBe( AlgorithmType.CLASSIC, From 5865464c53540a4020b17bbcf8772b5cbb89c585 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 26 Apr 2023 12:10:22 +0200 Subject: [PATCH 04/19] refactor; create ad request validation --- .../primaries/ad-messager.controller.ts | 32 ++++++++--- .../ad/domain/dtos/create-ad.request.ts | 55 ++++++++++++++----- src/modules/ad/domain/entities/ad.ts | 22 +++++--- src/modules/ad/domain/types/frequency.enum.ts | 4 ++ .../ad/domain/usecases/create-ad.usecase.ts | 16 +++++- src/modules/ad/mappers/ad.profile.ts | 25 ++++++++- .../domain/types/coordinates.type.ts | 4 ++ .../domain/types/point-type.enum.ts} | 0 .../geography/domain/types/point.type.ts | 6 ++ .../secondaries/graphhopper-georouter.ts | 2 +- .../matcher/domain/dtos/match.request.ts | 2 +- .../entities/ecosystem/algorithm-settings.ts | 6 +- .../domain/entities/ecosystem/geography.ts | 4 +- .../domain/entities/ecosystem/route.ts | 2 +- .../entities/ecosystem/spacetime-point.ts | 8 ++- .../matcher/domain/entities/ecosystem/time.ts | 16 +++--- .../domain/entities/ecosystem/waypoint.ts | 2 +- .../interfaces/geography-request.interface.ts | 2 +- src/modules/matcher/domain/types/day.type.ts | 9 +++ .../matcher/domain/types/days.const.ts | 1 + src/modules/matcher/domain/types/path.type.ts | 2 +- .../matcher/domain/types/point.type.ts | 7 --- src/modules/matcher/domain/types/timing.ts | 16 ------ src/modules/matcher/domain/types/waypoint.ts | 2 +- ...finder.spec.ts => timezone-finder.spec.ts} | 2 +- .../unit/domain/ecosystem/geography.spec.ts | 2 +- .../tests/unit/domain/ecosystem/route.spec.ts | 4 +- .../tests/unit/queries/match.query.spec.ts | 6 +- 28 files changed, 173 insertions(+), 86 deletions(-) create mode 100644 src/modules/ad/domain/types/frequency.enum.ts create mode 100644 src/modules/geography/domain/types/coordinates.type.ts rename src/modules/{matcher/domain/types/geography.enum.ts => geography/domain/types/point-type.enum.ts} (100%) create mode 100644 src/modules/geography/domain/types/point.type.ts create mode 100644 src/modules/matcher/domain/types/day.type.ts create mode 100644 src/modules/matcher/domain/types/days.const.ts delete mode 100644 src/modules/matcher/domain/types/point.type.ts delete mode 100644 src/modules/matcher/domain/types/timing.ts rename src/modules/matcher/tests/unit/adapters/secondaries/{geo-timezone-finder.spec.ts => timezone-finder.spec.ts} (96%) diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts index 5a09dd4..24bef87 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -5,28 +5,44 @@ import { InjectMapper } from '@automapper/nestjs'; import { Mapper } from '@automapper/core'; import { CommandBus } from '@nestjs/cqrs'; import { CreateAdCommand } from '../../commands/create-ad.command'; -import { AdPresenter } from './ad.presenter'; import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; +import { ValidationError, validateOrReject } from 'class-validator'; @Controller() export class AdMessagerController { constructor( - private readonly _commandBus: CommandBus, - @InjectMapper() private readonly _mapper: Mapper, + private readonly commandBus: CommandBus, + @InjectMapper() private readonly mapper: Mapper, ) {} @RabbitSubscribe({ name: 'adCreated', }) - async adCreatedHandler(message: string): Promise { + async adCreatedHandler(message: string): Promise { try { - const createAdRequest: CreateAdRequest = JSON.parse(message); - const ad: Ad = await this._commandBus.execute( + // parse message to conform to CreateAdRequest (not a real instance yet) + const parsedMessage: CreateAdRequest = JSON.parse(message); + // create a real instance of CreateAdRequest from parsed message + const createAdRequest: CreateAdRequest = this.mapper.map( + parsedMessage, + CreateAdRequest, + CreateAdRequest, + ); + console.log(createAdRequest); + // validate instance + await validateOrReject(createAdRequest); + const ad: Ad = await this.commandBus.execute( new CreateAdCommand(createAdRequest), ); - return this._mapper.map(ad, Ad, AdPresenter); + console.log(ad); } catch (e) { - console.log('error', e); + if (Array.isArray(e)) { + e.forEach((error) => + error instanceof ValidationError + ? console.log(error.constraints) + : console.log(error), + ); + } } } } diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index 1b0e230..dd4ea53 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -1,5 +1,18 @@ import { AutoMap } from '@automapper/classes'; -import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { PointType } from '../../../geography/domain/types/point-type.enum'; +import { Frequency } from '../types/frequency.enum'; +import { Point } from '../../../geography/domain/types/point.type'; export class CreateAdRequest { @IsString() @@ -15,9 +28,10 @@ export class CreateAdRequest { @AutoMap() passenger: boolean; - @IsNumber() + @IsNotEmpty() + @IsEnum(Frequency) @AutoMap() - frequency: number; + frequency: Frequency; @IsString() @AutoMap() @@ -27,33 +41,40 @@ export class CreateAdRequest { @AutoMap() toDate: string; + @IsOptional() @IsString() @AutoMap() - monTime: string; + monTime: string | null; + @IsOptional() @IsString() @AutoMap() - tueTime: string; + tueTime: string | null; + @IsOptional() @IsString() @AutoMap() - wedTime: string; + wedTime: string | null; + @IsOptional() @IsString() @AutoMap() - thuTime: string; + thuTime!: string | null; + @IsOptional() @IsString() @AutoMap() - friTime: string; + friTime: string | null; + @IsOptional() @IsString() @AutoMap() - satTime: string; + satTime: string | null; + @IsOptional() @IsString() @AutoMap() - sunTime: string; + sunTime: string | null; @IsNumber() @AutoMap() @@ -83,16 +104,19 @@ export class CreateAdRequest { @AutoMap() sunMargin: number; - @IsNumber() + @IsEnum(PointType) @AutoMap() - originType: number; + originType: PointType; - @IsNumber() + @IsEnum(PointType) @AutoMap() - destinationType: number; + destinationType: PointType; + @IsArray() + @ArrayMinSize(2) + @ValidateNested({ each: true }) @AutoMap() - waypoints: []; + waypoints: Array; @IsNumber() @AutoMap() @@ -102,6 +126,7 @@ export class CreateAdRequest { @AutoMap() seatsPassenger: number; + @IsOptional() @IsNumber() @AutoMap() seatsUsed: number; diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index e8998ec..817738e 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -1,4 +1,7 @@ import { AutoMap } from '@automapper/classes'; +import { ArrayMinSize, IsArray, IsEnum, ValidateNested } from 'class-validator'; +import { PointType } from '../../../geography/domain/types/point-type.enum'; +import { Point } from '../../../geography/domain/types/point.type'; export class Ad { @AutoMap() @@ -14,10 +17,10 @@ export class Ad { frequency: number; @AutoMap() - fromDate: string; + fromDate: Date; @AutoMap() - toDate: string; + toDate: Date; @AutoMap() monTime: string; @@ -73,14 +76,19 @@ export class Ad { @AutoMap() passengerDistance: number; + @IsEnum(PointType) @AutoMap() - originType: number; + originType: PointType; + @IsEnum(PointType) @AutoMap() - destinationType: number; + destinationType: PointType; + @IsArray() + @ArrayMinSize(2) + @ValidateNested({ each: true }) @AutoMap() - waypoints: []; + waypoints: Array; @AutoMap() direction: string; @@ -101,8 +109,8 @@ export class Ad { seatsUsed: number; @AutoMap() - createdAt: string; + createdAt: Date; @AutoMap() - updatedAt: string; + updatedAt: Date; } 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..ec435e8 --- /dev/null +++ b/src/modules/ad/domain/types/frequency.enum.ts @@ -0,0 +1,4 @@ +export enum Frequency { + PUNCTUAL = 1, + RECURRENT = 2, +} diff --git a/src/modules/ad/domain/usecases/create-ad.usecase.ts b/src/modules/ad/domain/usecases/create-ad.usecase.ts index b9b3b9e..d85bf81 100644 --- a/src/modules/ad/domain/usecases/create-ad.usecase.ts +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -2,14 +2,26 @@ 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'; @CommandHandler(CreateAdCommand) export class CreateAdUseCase { - constructor(private readonly adRepository: AdRepository) {} + constructor( + @InjectMapper() private readonly mapper: Mapper, + private readonly adRepository: AdRepository, + ) {} async execute(command: CreateAdCommand): Promise { try { - return await this.adRepository.createAd(command.createAdRequest); + const adToCreate: Ad = this.mapper.map( + command.createAdRequest, + CreateAdRequest, + Ad, + ); + return adToCreate; + // return await this.adRepository.createAd(adToCreate); } catch (error) { throw error; } diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index 289adbc..09a247f 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -1,8 +1,9 @@ -import { createMap, Mapper } from '@automapper/core'; +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; +import { CreateAdRequest } from '../domain/dtos/create-ad.request'; @Injectable() export class AdProfile extends AutomapperProfile { @@ -13,6 +14,28 @@ export class AdProfile extends AutomapperProfile { override get profile() { return (mapper: any) => { createMap(mapper, Ad, AdPresenter); + createMap(mapper, CreateAdRequest, CreateAdRequest); + createMap( + mapper, + CreateAdRequest, + Ad, + forMember( + (dest) => dest.fromDate, + mapFrom((source) => new Date(source.fromDate)), + ), + forMember( + (dest) => dest.toDate, + mapFrom((source) => new Date(source.toDate)), + ), + forMember( + (dest) => dest.createdAt, + mapFrom((source) => new Date(source.createdAt)), + ), + forMember( + (dest) => dest.updatedAt, + mapFrom((source) => new Date(source.updatedAt)), + ), + ); }; } } diff --git a/src/modules/geography/domain/types/coordinates.type.ts b/src/modules/geography/domain/types/coordinates.type.ts new file mode 100644 index 0000000..8e149ed --- /dev/null +++ b/src/modules/geography/domain/types/coordinates.type.ts @@ -0,0 +1,4 @@ +export type Coordinates = { + lon: number; + lat: number; +}; 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..9285d70 --- /dev/null +++ b/src/modules/geography/domain/types/point.type.ts @@ -0,0 +1,6 @@ +import { PointType } from './point-type.enum'; +import { Coordinates } from './coordinates.type'; + +export type Point = Coordinates & { + type?: PointType; +}; diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 9d85e85..2475a43 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -154,7 +154,7 @@ export class GraphhopperGeorouter implements IGeorouter { 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, ), diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 4cb1ff2..0ce4127 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -10,7 +10,7 @@ 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'; diff --git a/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts index a6d4963..158ddfd 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,7 +22,7 @@ export class AlgorithmSettings { constructor( algorithmSettingsRequest: IRequestAlgorithmSettings, defaultAlgorithmSettings: DefaultAlgorithmSettings, - frequency: TimingFrequency, + frequency: Frequency, georouterCreator: ICreateGeorouter, ) { this.algorithmSettingsRequest = algorithmSettingsRequest; diff --git a/src/modules/matcher/domain/entities/ecosystem/geography.ts b/src/modules/matcher/domain/entities/ecosystem/geography.ts index 56a08a5..59af204 100644 --- a/src/modules/matcher/domain/entities/ecosystem/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -3,8 +3,8 @@ 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 { PointType } from '../../../../geography/domain/types/point-type.enum'; +import { Point } from '../../../../geography/domain/types/point.type'; import { Route } from './route'; import { Role } from '../../types/role.enum'; import { IGeorouter } from '../../interfaces/georouter.interface'; diff --git a/src/modules/matcher/domain/entities/ecosystem/route.ts b/src/modules/matcher/domain/entities/ecosystem/route.ts index 2bb27b8..f9670e4 100644 --- a/src/modules/matcher/domain/entities/ecosystem/route.ts +++ b/src/modules/matcher/domain/entities/ecosystem/route.ts @@ -1,5 +1,5 @@ import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface'; -import { Point } from '../../types/point.type'; +import { Point } from '../../../../geography/domain/types/point.type'; import { SpacetimePoint } from './spacetime-point'; import { Waypoint } from './waypoint'; diff --git a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts index 98fe80f..57e21d6 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 { Coordinates } from 'src/modules/geography/domain/types/coordinates.type'; + export class SpacetimePoint { - point: Array; + coordinates: Coordinates; duration: number; distance: number; - constructor(point: Array, duration: number, distance: number) { - this.point = point; + constructor(coordinates: Coordinates, duration: number, distance: number) { + this.coordinates = coordinates; 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..417cd22 100644 --- a/src/modules/matcher/domain/entities/ecosystem/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -4,14 +4,16 @@ 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 { DAYS } from '../../types/days.const'; import { Schedule } from '../../types/schedule.type'; +import { Frequency } from '../../../../ad/domain/types/frequency.enum'; +import { Day } from '../../types/day.type'; export class Time { private timeRequest: IRequestTime; private defaultMarginDuration: number; private defaultValidityDuration: number; - frequency: TimingFrequency; + frequency: Frequency; fromDate: Date; toDate: Date; schedule: Schedule; @@ -106,7 +108,7 @@ export class Time { } if ( !Object.keys(this.timeRequest.schedule).some((elem) => - Days.includes(elem), + DAYS.includes(elem), ) ) { throw new MatcherException( @@ -127,15 +129,15 @@ export class Time { private setPunctualRequest = (): void => { if (this.timeRequest.departure) { - this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; - this.schedule[TimingDays[this.fromDate.getDay()]] = + this.frequency = Frequency.PUNCTUAL; + this.schedule[Day[this.fromDate.getDay()]] = this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); } }; 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); } @@ -165,7 +167,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/waypoint.ts b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts index fdcbea0..3695dc2 100644 --- a/src/modules/matcher/domain/entities/ecosystem/waypoint.ts +++ b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts @@ -1,4 +1,4 @@ -import { Point } from '../../types/point.type'; +import { Point } from '../../../../geography/domain/types/point.type'; import { Actor } from './actor'; export class Waypoint { diff --git a/src/modules/matcher/domain/interfaces/geography-request.interface.ts b/src/modules/matcher/domain/interfaces/geography-request.interface.ts index d10a6ac..6cf3673 100644 --- a/src/modules/matcher/domain/interfaces/geography-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/geography-request.interface.ts @@ -1,4 +1,4 @@ -import { Point } from '../types/point.type'; +import { Point } from '../../../geography/domain/types/point.type'; export interface IRequestGeography { waypoints: Array; 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/path.type.ts b/src/modules/matcher/domain/types/path.type.ts index 8a1bfe9..49437bd 100644 --- a/src/modules/matcher/domain/types/path.type.ts +++ b/src/modules/matcher/domain/types/path.type.ts @@ -1,4 +1,4 @@ -import { Point } from './point.type'; +import { Point } from '../../../geography/domain/types/point.type'; export type Path = { key: string; 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/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..8628fd1 100644 --- a/src/modules/matcher/domain/types/waypoint.ts +++ b/src/modules/matcher/domain/types/waypoint.ts @@ -1,5 +1,5 @@ import { Actor } from './actor.type.'; -import { Point } from './point.type'; +import { Point } from '../../../geography/domain/types/point.type'; export type Waypoint = { point: Point; diff --git a/src/modules/matcher/tests/unit/adapters/secondaries/geo-timezone-finder.spec.ts b/src/modules/matcher/tests/unit/adapters/secondaries/timezone-finder.spec.ts similarity index 96% rename from src/modules/matcher/tests/unit/adapters/secondaries/geo-timezone-finder.spec.ts rename to src/modules/matcher/tests/unit/adapters/secondaries/timezone-finder.spec.ts index 5dccb46..63d8462 100644 --- a/src/modules/matcher/tests/unit/adapters/secondaries/geo-timezone-finder.spec.ts +++ b/src/modules/matcher/tests/unit/adapters/secondaries/timezone-finder.spec.ts @@ -6,7 +6,7 @@ const mockGeoTimezoneFinder = { timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), }; -describe('Geo TZ Finder', () => { +describe('Timezone Finder', () => { let timezoneFinder: TimezoneFinder; beforeAll(async () => { 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 2503dcc..9b870e8 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -7,7 +7,7 @@ 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 '../../../../../geography/domain/interfaces/geodesic.interface'; -import { PointType } from '../../../../domain/types/geography.enum'; +import { PointType } from '../../../../../geography/domain/types/point-type.enum'; const person: Person = new Person( { diff --git a/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts index 16d27a3..70c9460 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/route.spec.ts @@ -57,8 +57,8 @@ describe('Route entity', () => { }); 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 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/queries/match.query.spec.ts b/src/modules/matcher/tests/unit/queries/match.query.spec.ts index 92dae34..7640929 100644 --- a/src/modules/matcher/tests/unit/queries/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -1,9 +1,9 @@ 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'; const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, @@ -209,9 +209,7 @@ describe('Match query', () => { 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(); From 96577e119fe1b560c483b5853605e93a0e4d26eb Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 26 Apr 2023 14:14:46 +0200 Subject: [PATCH 05/19] refactor --- .../primaries/ad-messager.controller.ts | 1 + .../ad/domain/dtos/create-ad.request.ts | 8 ++-- src/modules/ad/domain/entities/ad.ts | 12 ++--- .../configuration-messager.controller.ts | 2 +- .../secondaries/prisma-repository.abstract.ts | 6 +-- .../secondaries/geo-timezone-finder.ts | 2 +- .../interfaces/timezone-finder.interface.ts | 2 +- .../domain/types/coordinates.type.ts | 21 +++++++- .../secondaries/graphhopper-georouter.ts | 48 +++++++++---------- .../adapters/secondaries/timezone-finder.ts | 2 +- .../matcher/domain/dtos/match.request.ts | 4 +- .../domain/entities/ecosystem/geography.ts | 17 +++---- .../domain/entities/ecosystem/person.ts | 4 +- .../domain/entities/ecosystem/route.ts | 14 +++--- .../domain/entities/ecosystem/waypoint.ts | 2 +- .../factory/algorithm-factory.abstract.ts | 4 +- .../domain/entities/engine/factory/classic.ts | 2 +- .../matcher/domain/entities/engine/matcher.ts | 4 +- .../classic-waypoint.completer.processor.ts | 2 +- .../processor/completer/completer.abstract.ts | 5 +- .../completer/journey.completer.processor.ts | 2 +- .../completer/route.completer.processor.ts | 2 +- .../processor/filter/filter.abstract.ts | 5 +- .../geofilter/classic.filter.processor.ts | 2 +- .../timefilter/classic.filter.processor.ts | 2 +- .../engine/processor/processor.abstract.ts | 2 +- .../engine/selector/classic.selector.ts | 2 +- .../engine/selector/selector.abstract.ts | 2 +- .../interfaces/geography-request.interface.ts | 2 +- .../domain/interfaces/georouter.interface.ts | 5 +- src/modules/matcher/domain/types/path.type.ts | 2 +- src/modules/matcher/domain/types/waypoint.ts | 2 +- .../matcher/domain/usecases/match.usecase.ts | 2 +- src/modules/matcher/queries/match.query.ts | 4 +- 34 files changed, 100 insertions(+), 98 deletions(-) diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts index 24bef87..7dc19ce 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -22,6 +22,7 @@ export class AdMessagerController { try { // parse message to conform to CreateAdRequest (not a real instance yet) const parsedMessage: CreateAdRequest = JSON.parse(message); + console.log(parsedMessage); // create a real instance of CreateAdRequest from parsed message const createAdRequest: CreateAdRequest = this.mapper.map( parsedMessage, diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index dd4ea53..f05c67f 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -8,11 +8,10 @@ import { IsNumber, IsOptional, IsString, - ValidateNested, } from 'class-validator'; import { PointType } from '../../../geography/domain/types/point-type.enum'; import { Frequency } from '../types/frequency.enum'; -import { Point } from '../../../geography/domain/types/point.type'; +import { Coordinates } from '../../../geography/domain/types/coordinates.type'; export class CreateAdRequest { @IsString() @@ -114,9 +113,8 @@ export class CreateAdRequest { @IsArray() @ArrayMinSize(2) - @ValidateNested({ each: true }) - @AutoMap() - waypoints: Array; + @AutoMap(() => [Coordinates]) + waypoints: Coordinates[]; @IsNumber() @AutoMap() diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index 817738e..98771fb 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -1,7 +1,6 @@ import { AutoMap } from '@automapper/classes'; -import { ArrayMinSize, IsArray, IsEnum, ValidateNested } from 'class-validator'; import { PointType } from '../../../geography/domain/types/point-type.enum'; -import { Point } from '../../../geography/domain/types/point.type'; +import { Coordinates } from '../../../geography/domain/types/coordinates.type'; export class Ad { @AutoMap() @@ -76,19 +75,14 @@ export class Ad { @AutoMap() passengerDistance: number; - @IsEnum(PointType) @AutoMap() originType: PointType; - @IsEnum(PointType) @AutoMap() destinationType: PointType; - @IsArray() - @ArrayMinSize(2) - @ValidateNested({ each: true }) - @AutoMap() - waypoints: Array; + @AutoMap(() => [Coordinates]) + waypoints: Coordinates[]; @AutoMap() direction: string; 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/src/adapters/secondaries/prisma-repository.abstract.ts index e93f896..dee142c 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -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 } 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, diff --git a/src/modules/geography/adapters/secondaries/geo-timezone-finder.ts b/src/modules/geography/adapters/secondaries/geo-timezone-finder.ts index 1ca02c2..bce0097 100644 --- a/src/modules/geography/adapters/secondaries/geo-timezone-finder.ts +++ b/src/modules/geography/adapters/secondaries/geo-timezone-finder.ts @@ -4,5 +4,5 @@ import { find } from 'geo-tz'; @Injectable() export class GeoTimezoneFinder implements IFindTimezone { - timezones = (lon: number, lat: number): Array => find(lat, lon); + timezones = (lon: number, lat: number): string[] => find(lat, lon); } diff --git a/src/modules/geography/domain/interfaces/timezone-finder.interface.ts b/src/modules/geography/domain/interfaces/timezone-finder.interface.ts index 6f22169..61016f7 100644 --- a/src/modules/geography/domain/interfaces/timezone-finder.interface.ts +++ b/src/modules/geography/domain/interfaces/timezone-finder.interface.ts @@ -1,3 +1,3 @@ export interface IFindTimezone { - timezones(lon: number, lat: number): Array; + timezones(lon: number, lat: number): string[]; } diff --git a/src/modules/geography/domain/types/coordinates.type.ts b/src/modules/geography/domain/types/coordinates.type.ts index 8e149ed..80872c6 100644 --- a/src/modules/geography/domain/types/coordinates.type.ts +++ b/src/modules/geography/domain/types/coordinates.type.ts @@ -1,4 +1,21 @@ -export type Coordinates = { +import { AutoMap } from '@automapper/classes'; +import { IsNumber, Max, Min } from 'class-validator'; + +export class Coordinates { + constructor(lon: number, lat: number) { + this.lon = lon; + this.lat = lat; + } + + @IsNumber() + @Min(-180) + @Max(180) + @AutoMap() lon: number; + + @IsNumber() + @Min(-90) + @Max(90) + @AutoMap() lat: number; -}; +} diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 2475a43..935eddb 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -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 = [ @@ -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,11 +143,11 @@ 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); @@ -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/timezone-finder.ts b/src/modules/matcher/adapters/secondaries/timezone-finder.ts index 5ce3306..8459661 100644 --- a/src/modules/matcher/adapters/secondaries/timezone-finder.ts +++ b/src/modules/matcher/adapters/secondaries/timezone-finder.ts @@ -6,6 +6,6 @@ import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-fin export class TimezoneFinder implements IFindTimezone { constructor(private readonly geoTimezoneFinder: GeoTimezoneFinder) {} - timezones = (lon: number, lat: number): Array => + 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 0ce4127..5ffea23 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -30,7 +30,7 @@ export class MatchRequest { @IsArray() @AutoMap() - waypoints: Array; + waypoints: Point[]; @IsOptional() @IsString() @@ -138,7 +138,7 @@ export class MatchRequest @IsOptional() @IsArray() - exclusions: Array; + exclusions: number[]; @IsOptional() @IsInt() diff --git a/src/modules/matcher/domain/entities/ecosystem/geography.ts b/src/modules/matcher/domain/entities/ecosystem/geography.ts index 59af204..68d1a39 100644 --- a/src/modules/matcher/domain/entities/ecosystem/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -19,10 +19,10 @@ import { Timezoner } from './timezoner'; export class Geography { private geographyRequest: IRequestGeography; private person: Person; - private points: Array; + private points: Point[]; originType: PointType; destinationType: PointType; - timezones: Array; + timezones: string[]; driverRoute: Route; passengerRoute: Route; timezoneFinder: IFindTimezone; @@ -48,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 @@ -174,10 +174,7 @@ 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) { diff --git a/src/modules/matcher/domain/entities/ecosystem/person.ts b/src/modules/matcher/domain/entities/ecosystem/person.ts index c6baa02..2f0906c 100644 --- a/src/modules/matcher/domain/entities/ecosystem/person.ts +++ b/src/modules/matcher/domain/entities/ecosystem/person.ts @@ -5,7 +5,7 @@ export class Person { private defaultIdentifier: number; private defaultMarginDuration: number; identifier: number; - marginDurations: Array; + marginDurations: number[]; constructor( personRequest: IRequestPerson, @@ -34,7 +34,7 @@ export class Person { this.identifier = identifier; }; - setMarginDurations = (marginDurations: Array): void => { + setMarginDurations = (marginDurations: number[]): void => { this.marginDurations = marginDurations; }; } diff --git a/src/modules/matcher/domain/entities/ecosystem/route.ts b/src/modules/matcher/domain/entities/ecosystem/route.ts index f9670e4..c42dba9 100644 --- a/src/modules/matcher/domain/entities/ecosystem/route.ts +++ b/src/modules/matcher/domain/entities/ecosystem/route.ts @@ -9,9 +9,9 @@ export class Route { fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - waypoints: Array; - points: Array; - spacetimePoints: Array; + waypoints: Waypoint[]; + points: Point[]; + spacetimePoints: SpacetimePoint[]; private geodesic: IGeodesic; constructor(geodesic: IGeodesic) { @@ -26,21 +26,21 @@ export class Route { this.geodesic = geodesic; } - setWaypoints = (waypoints: Array): void => { + setWaypoints = (waypoints: Waypoint[]): 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 => { + private setAzimuth = (points: Point[]): void => { const inverse = this.geodesic.inverse( points[0].lon, points[0].lat, diff --git a/src/modules/matcher/domain/entities/ecosystem/waypoint.ts b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts index 3695dc2..48c0899 100644 --- a/src/modules/matcher/domain/entities/ecosystem/waypoint.ts +++ b/src/modules/matcher/domain/entities/ecosystem/waypoint.ts @@ -3,7 +3,7 @@ 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/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/geography-request.interface.ts b/src/modules/matcher/domain/interfaces/geography-request.interface.ts index 6cf3673..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 '../../../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/types/path.type.ts b/src/modules/matcher/domain/types/path.type.ts index 49437bd..44e03b6 100644 --- a/src/modules/matcher/domain/types/path.type.ts +++ b/src/modules/matcher/domain/types/path.type.ts @@ -2,5 +2,5 @@ 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/waypoint.ts b/src/modules/matcher/domain/types/waypoint.ts index 8628fd1..bc15ea5 100644 --- a/src/modules/matcher/domain/types/waypoint.ts +++ b/src/modules/matcher/domain/types/waypoint.ts @@ -3,5 +3,5 @@ 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..c834224 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -17,7 +17,7 @@ export class MatchUseCase { execute = async (matchQuery: MatchQuery): Promise> => { try { - const data: Array = await this._matcher.match(matchQuery); + const data: Match[] = await this._matcher.match(matchQuery); this._messager.publish('matcher.match', 'match !'); return { data, diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 3f7c8dd..7de0009 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -15,10 +15,10 @@ export class MatchQuery { private readonly defaultParams: IDefaultParams; private readonly georouterCreator: ICreateGeorouter; person: Person; - roles: Array; + roles: Role[]; time: Time; geography: Geography; - exclusions: Array; + exclusions: number[]; requirement: Requirement; algorithmSettings: AlgorithmSettings; georouter: IGeorouter; From 95310651d8e590e2f86d6e540fedddc0c22a1370 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 27 Apr 2023 17:52:43 +0200 Subject: [PATCH 06/19] wip --- .../primaries/ad-messager.controller.ts | 19 +++++++++++------ .../ad/domain/dtos/create-ad.request.ts | 4 ++++ src/modules/ad/mappers/ad.profile.ts | 21 ++++++++++++++++++- src/modules/ad/mappers/coordinates.profile.ts | 17 +++++++++++++++ .../domain/types/coordinates.type.ts | 8 +++---- 5 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 src/modules/ad/mappers/coordinates.profile.ts diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts index 7dc19ce..bbc4685 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -24,14 +24,21 @@ export class AdMessagerController { const parsedMessage: CreateAdRequest = JSON.parse(message); console.log(parsedMessage); // create a real instance of CreateAdRequest from parsed message - const createAdRequest: CreateAdRequest = this.mapper.map( - parsedMessage, - CreateAdRequest, - CreateAdRequest, - ); + // const createAdRequest: CreateAdRequest = this.mapper.map( + // parsedMessage, + // CreateAdRequest, + // CreateAdRequest, + // ); + const createAdRequest = new CreateAdRequest(); + createAdRequest.originType = parsedMessage.originType; + createAdRequest.destinationType = parsedMessage.destinationType; + createAdRequest.waypoints = parsedMessage.waypoints.map((waypoint) => ({ + lon: waypoint.lon, + lat: waypoint.lat, + })); console.log(createAdRequest); // validate instance - await validateOrReject(createAdRequest); + await validateOrReject(createAdRequest.waypoints[0]); const ad: Ad = await this.commandBus.execute( new CreateAdCommand(createAdRequest), ); diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index f05c67f..0e71c3d 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -8,10 +8,12 @@ import { IsNumber, IsOptional, IsString, + ValidateNested, } from 'class-validator'; import { PointType } from '../../../geography/domain/types/point-type.enum'; import { Frequency } from '../types/frequency.enum'; import { Coordinates } from '../../../geography/domain/types/coordinates.type'; +import { Type } from 'class-transformer'; export class CreateAdRequest { @IsString() @@ -112,7 +114,9 @@ export class CreateAdRequest { destinationType: PointType; @IsArray() + @ValidateNested({ each: true }) @ArrayMinSize(2) + @Type(() => Coordinates) @AutoMap(() => [Coordinates]) waypoints: Coordinates[]; diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index 09a247f..460068c 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -4,6 +4,7 @@ import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; +import { Coordinates } from 'src/modules/geography/domain/types/coordinates.type'; @Injectable() export class AdProfile extends AutomapperProfile { @@ -14,7 +15,25 @@ export class AdProfile extends AutomapperProfile { override get profile() { return (mapper: any) => { createMap(mapper, Ad, AdPresenter); - createMap(mapper, CreateAdRequest, CreateAdRequest); + createMap( + mapper, + CreateAdRequest, + CreateAdRequest, + forMember( + (dest) => dest.waypoints, + mapFrom( + (source) => + source.waypoints.map( + (waypoint) => + new Coordinates( + waypoint.lon ?? undefined, + waypoint.lat ?? undefined, + ), + ), + // .filter((waypoint) => waypoint), + ), + ), + ); createMap( mapper, CreateAdRequest, diff --git a/src/modules/ad/mappers/coordinates.profile.ts b/src/modules/ad/mappers/coordinates.profile.ts new file mode 100644 index 0000000..69a4a47 --- /dev/null +++ b/src/modules/ad/mappers/coordinates.profile.ts @@ -0,0 +1,17 @@ +import { createMap, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { Coordinates } from '../../geography/domain/types/coordinates.type'; + +@Injectable() +export class CoordinatesProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper: any) => { + createMap(mapper, Coordinates, Coordinates); + }; + } +} diff --git a/src/modules/geography/domain/types/coordinates.type.ts b/src/modules/geography/domain/types/coordinates.type.ts index 80872c6..05769c2 100644 --- a/src/modules/geography/domain/types/coordinates.type.ts +++ b/src/modules/geography/domain/types/coordinates.type.ts @@ -1,5 +1,5 @@ import { AutoMap } from '@automapper/classes'; -import { IsNumber, Max, Min } from 'class-validator'; +import { IsLatitude, IsLongitude, IsNumber } from 'class-validator'; export class Coordinates { constructor(lon: number, lat: number) { @@ -8,14 +8,12 @@ export class Coordinates { } @IsNumber() - @Min(-180) - @Max(180) + @IsLongitude() @AutoMap() lon: number; @IsNumber() - @Min(-90) - @Max(90) + @IsLatitude() @AutoMap() lat: number; } From 1f9a9896e95a3df1a2596250c7e4943d9b81322e Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 28 Apr 2023 15:53:57 +0200 Subject: [PATCH 07/19] wip --- package-lock.json | 16 +++++- package.json | 3 +- src/modules/ad/ad.modules.ts | 12 ++++- .../primaries/ad-messager.controller.ts | 54 +++++++++++-------- .../ad/adapters/secondaries/message-broker.ts | 12 +++++ .../ad/adapters/secondaries/messager.ts | 18 +++++++ .../adapters/secondaries/timezone-finder.ts | 11 ++++ .../ad/domain/dtos/create-ad.request.ts | 8 ++- src/modules/ad/domain/entities/ad.ts | 4 +- src/modules/ad/mappers/ad.profile.ts | 33 ++++++++---- src/modules/ad/mappers/coordinates.profile.ts | 17 ------ .../coordinates.ts} | 0 .../geography/domain/types/point.type.ts | 2 +- .../entities/ecosystem/spacetime-point.ts | 2 +- .../matcher/domain/entities/ecosystem/time.ts | 2 - 15 files changed, 129 insertions(+), 65 deletions(-) create mode 100644 src/modules/ad/adapters/secondaries/message-broker.ts create mode 100644 src/modules/ad/adapters/secondaries/messager.ts create mode 100644 src/modules/ad/adapters/secondaries/timezone-finder.ts delete mode 100644 src/modules/ad/mappers/coordinates.profile.ts rename src/modules/geography/domain/{types/coordinates.type.ts => entities/coordinates.ts} (100%) 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/src/modules/ad/ad.modules.ts b/src/modules/ad/ad.modules.ts index e59f7fd..91717d5 100644 --- a/src/modules/ad/ad.modules.ts +++ b/src/modules/ad/ad.modules.ts @@ -7,6 +7,9 @@ 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 { TimezoneFinder } from './adapters/secondaries/timezone-finder'; +import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder'; @Module({ imports: [ @@ -36,7 +39,14 @@ import { CqrsModule } from '@nestjs/cqrs'; }), ], controllers: [AdMessagerController], - providers: [AdProfile, AdRepository, CreateAdUseCase], + providers: [ + AdProfile, + Messager, + AdRepository, + TimezoneFinder, + GeoTimezoneFinder, + 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 index bbc4685..4028b69 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -6,51 +6,59 @@ import { Mapper } from '@automapper/core'; import { CommandBus } from '@nestjs/cqrs'; import { CreateAdCommand } from '../../commands/create-ad.command'; import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; -import { ValidationError, validateOrReject } from 'class-validator'; +import { validateOrReject } from 'class-validator'; +import { Messager } from '../secondaries/messager'; +import { GeoTimezoneFinder } from 'src/modules/geography/adapters/secondaries/geo-timezone-finder'; @Controller() export class AdMessagerController { constructor( + private readonly messager: Messager, private readonly commandBus: CommandBus, @InjectMapper() private readonly mapper: Mapper, + private readonly timezoneFinder: GeoTimezoneFinder, ) {} @RabbitSubscribe({ name: 'adCreated', }) async adCreatedHandler(message: string): Promise { + let createAdRequest: CreateAdRequest; try { // parse message to conform to CreateAdRequest (not a real instance yet) const parsedMessage: CreateAdRequest = JSON.parse(message); - console.log(parsedMessage); // create a real instance of CreateAdRequest from parsed message - // const createAdRequest: CreateAdRequest = this.mapper.map( - // parsedMessage, - // CreateAdRequest, - // CreateAdRequest, - // ); - const createAdRequest = new CreateAdRequest(); - createAdRequest.originType = parsedMessage.originType; - createAdRequest.destinationType = parsedMessage.destinationType; - createAdRequest.waypoints = parsedMessage.waypoints.map((waypoint) => ({ - lon: waypoint.lon, - lat: waypoint.lat, - })); - console.log(createAdRequest); + createAdRequest = this.mapper.map( + parsedMessage, + CreateAdRequest, + CreateAdRequest, + ); // validate instance - await validateOrReject(createAdRequest.waypoints[0]); + 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; + } + } + createAdRequest.timezone = this.timezoneFinder.timezones( + createAdRequest.waypoints[0].lon, + createAdRequest.waypoints[0].lat, + )[0]; const ad: Ad = await this.commandBus.execute( new CreateAdCommand(createAdRequest), ); console.log(ad); } catch (e) { - if (Array.isArray(e)) { - e.forEach((error) => - error instanceof ValidationError - ? console.log(error.constraints) - : console.log(error), - ); - } + this.messager.publish( + 'logging.matcher.ad.crit', + JSON.stringify({ + createAdRequest, + error: e, + }), + ); } } } 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..96fa7cc --- /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/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index 0e71c3d..02fcd41 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -8,12 +8,10 @@ import { IsNumber, IsOptional, IsString, - ValidateNested, } from 'class-validator'; import { PointType } from '../../../geography/domain/types/point-type.enum'; import { Frequency } from '../types/frequency.enum'; -import { Coordinates } from '../../../geography/domain/types/coordinates.type'; -import { Type } from 'class-transformer'; +import { Coordinates } from '../../../geography/domain/entities/coordinates'; export class CreateAdRequest { @IsString() @@ -114,9 +112,7 @@ export class CreateAdRequest { destinationType: PointType; @IsArray() - @ValidateNested({ each: true }) @ArrayMinSize(2) - @Type(() => Coordinates) @AutoMap(() => [Coordinates]) waypoints: Coordinates[]; @@ -140,4 +136,6 @@ export class CreateAdRequest { @IsString() @AutoMap() updatedAt: string; + + timezone: string; } diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index 98771fb..f19b5e0 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { PointType } from '../../../geography/domain/types/point-type.enum'; -import { Coordinates } from '../../../geography/domain/types/coordinates.type'; +import { Coordinates } from '../../../geography/domain/entities/coordinates'; export class Ad { @AutoMap() @@ -22,7 +22,7 @@ export class Ad { toDate: Date; @AutoMap() - monTime: string; + monTime: Date; @AutoMap() tueTime: string; diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index 460068c..3bd2eed 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -4,7 +4,8 @@ import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; -import { Coordinates } from 'src/modules/geography/domain/types/coordinates.type'; +import { Coordinates } from '../../geography/domain/entities/coordinates'; +import moment from 'moment-timezone'; @Injectable() export class AdProfile extends AutomapperProfile { @@ -21,16 +22,14 @@ export class AdProfile extends AutomapperProfile { CreateAdRequest, forMember( (dest) => dest.waypoints, - mapFrom( - (source) => - source.waypoints.map( - (waypoint) => - new Coordinates( - waypoint.lon ?? undefined, - waypoint.lat ?? undefined, - ), - ), - // .filter((waypoint) => waypoint), + mapFrom((source) => + source.waypoints.map( + (waypoint) => + new Coordinates( + waypoint.lon ?? undefined, + waypoint.lat ?? undefined, + ), + ), ), ), ); @@ -54,6 +53,18 @@ export class AdProfile extends AutomapperProfile { (dest) => dest.updatedAt, mapFrom((source) => new Date(source.updatedAt)), ), + // forMember( + // (dest) => dest.monTime, + // mapFrom((source) => + // source.monTime + // ? new Date( + // moment + // .tz(`${source.fromDate} ${source.monTime}`, source.timezone) + // .format(), + // ) + // : undefined, + // ), + // ), ); }; } diff --git a/src/modules/ad/mappers/coordinates.profile.ts b/src/modules/ad/mappers/coordinates.profile.ts deleted file mode 100644 index 69a4a47..0000000 --- a/src/modules/ad/mappers/coordinates.profile.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createMap, Mapper } from '@automapper/core'; -import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; -import { Injectable } from '@nestjs/common'; -import { Coordinates } from '../../geography/domain/types/coordinates.type'; - -@Injectable() -export class CoordinatesProfile extends AutomapperProfile { - constructor(@InjectMapper() mapper: Mapper) { - super(mapper); - } - - override get profile() { - return (mapper: any) => { - createMap(mapper, Coordinates, Coordinates); - }; - } -} diff --git a/src/modules/geography/domain/types/coordinates.type.ts b/src/modules/geography/domain/entities/coordinates.ts similarity index 100% rename from src/modules/geography/domain/types/coordinates.type.ts rename to src/modules/geography/domain/entities/coordinates.ts diff --git a/src/modules/geography/domain/types/point.type.ts b/src/modules/geography/domain/types/point.type.ts index 9285d70..b3733a8 100644 --- a/src/modules/geography/domain/types/point.type.ts +++ b/src/modules/geography/domain/types/point.type.ts @@ -1,5 +1,5 @@ import { PointType } from './point-type.enum'; -import { Coordinates } from './coordinates.type'; +import { Coordinates } from '../entities/coordinates'; export type Point = Coordinates & { type?: PointType; diff --git a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts index 57e21d6..91dd7dc 100644 --- a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts +++ b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts @@ -1,4 +1,4 @@ -import { Coordinates } from 'src/modules/geography/domain/types/coordinates.type'; +import { Coordinates } from '../../../../geography/domain/entities/coordinates'; export class SpacetimePoint { coordinates: Coordinates; diff --git a/src/modules/matcher/domain/entities/ecosystem/time.ts b/src/modules/matcher/domain/entities/ecosystem/time.ts index 417cd22..6059784 100644 --- a/src/modules/matcher/domain/entities/ecosystem/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -11,7 +11,6 @@ import { Day } from '../../types/day.type'; export class Time { private timeRequest: IRequestTime; - private defaultMarginDuration: number; private defaultValidityDuration: number; frequency: Frequency; fromDate: Date; @@ -25,7 +24,6 @@ export class Time { defaultValidityDuration: number, ) { this.timeRequest = timeRequest; - this.defaultMarginDuration = defaultMarginDuration; this.defaultValidityDuration = defaultValidityDuration; this.schedule = {}; this.marginDurations = { From e312a6c712809b8f5c75261991fb5e2ef4f2ee5d Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 2 May 2023 11:56:07 +0200 Subject: [PATCH 08/19] fix utc conversion for schedule --- src/modules/ad/domain/entities/ad.ts | 12 +-- .../ad/domain/entities/time-converter.ts | 16 ++++ src/modules/ad/mappers/ad.profile.ts | 84 ++++++++++++++++--- 3 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 src/modules/ad/domain/entities/time-converter.ts diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index f19b5e0..1e4d1d6 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -25,22 +25,22 @@ export class Ad { monTime: Date; @AutoMap() - tueTime: string; + tueTime: Date; @AutoMap() - wedTime: string; + wedTime: Date; @AutoMap() - thuTime: string; + thuTime: Date; @AutoMap() - friTime: string; + friTime: Date; @AutoMap() - satTime: string; + satTime: Date; @AutoMap() - sunTime: string; + sunTime: Date; @AutoMap() monMargin: number; 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..b5910e6 --- /dev/null +++ b/src/modules/ad/domain/entities/time-converter.ts @@ -0,0 +1,16 @@ +import { DateTime, TimeZone } from 'timezonecomplete'; + +export class TimeConverter { + static toUtcDatetime = ( + date: string, + time: string, + ianaTimezone: string, + ): Date => + date && time + ? new Date( + new DateTime(`${date}T${time}:00`, TimeZone.zone(ianaTimezone, false)) + .convert(TimeZone.zone('UTC')) + .toIsoString(), + ) + : undefined; +} diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index 3bd2eed..6cf8d8a 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -5,7 +5,7 @@ import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; import { Coordinates } from '../../geography/domain/entities/coordinates'; -import moment from 'moment-timezone'; +import { TimeConverter } from '../domain/entities/time-converter'; @Injectable() export class AdProfile extends AutomapperProfile { @@ -53,18 +53,76 @@ export class AdProfile extends AutomapperProfile { (dest) => dest.updatedAt, mapFrom((source) => new Date(source.updatedAt)), ), - // forMember( - // (dest) => dest.monTime, - // mapFrom((source) => - // source.monTime - // ? new Date( - // moment - // .tz(`${source.fromDate} ${source.monTime}`, source.timezone) - // .format(), - // ) - // : undefined, - // ), - // ), + forMember( + (dest) => dest.monTime, + mapFrom((source) => + TimeConverter.toUtcDatetime( + source.fromDate, + source.monTime, + source.timezone, + ), + ), + ), + forMember( + (dest) => dest.tueTime, + mapFrom((source) => + TimeConverter.toUtcDatetime( + source.fromDate, + source.tueTime, + source.timezone, + ), + ), + ), + forMember( + (dest) => dest.wedTime, + mapFrom((source) => + TimeConverter.toUtcDatetime( + source.fromDate, + source.wedTime, + source.timezone, + ), + ), + ), + forMember( + (dest) => dest.thuTime, + mapFrom((source) => + TimeConverter.toUtcDatetime( + source.fromDate, + source.thuTime, + source.timezone, + ), + ), + ), + forMember( + (dest) => dest.friTime, + mapFrom((source) => + TimeConverter.toUtcDatetime( + source.fromDate, + source.friTime, + source.timezone, + ), + ), + ), + forMember( + (dest) => dest.satTime, + mapFrom((source) => + TimeConverter.toUtcDatetime( + source.fromDate, + source.satTime, + source.timezone, + ), + ), + ), + forMember( + (dest) => dest.sunTime, + mapFrom((source) => + TimeConverter.toUtcDatetime( + source.fromDate, + source.sunTime, + source.timezone, + ), + ), + ), ); }; } From a6f7476599adfd46afd300cb4560812ac13af5bb Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 2 May 2023 17:26:04 +0200 Subject: [PATCH 09/19] add mode to match wip --- src/app.module.ts | 2 +- .../ad/{ad.modules.ts => ad.module.ts} | 0 .../ad/domain/dtos/create-ad.request.ts | 18 ++-- .../ad/domain/entities/time-converter.ts | 21 +++-- src/modules/ad/mappers/ad.profile.ts | 56 +++--------- .../adapters/secondaries/messager.spec.ts | 47 ++++++++++ .../secondaries/timezone-finder.spec.ts | 35 +++++++ .../unit/domain/create-ad.usecase.spec.ts | 91 +++++++++++++++++++ .../tests/unit/domain/time-converter.spec.ts | 35 +++++++ .../adapters/primaries/matcher.controller.ts | 3 + .../matcher/adapters/primaries/matcher.proto | 53 ++++++----- .../secondaries/default-params.provider.ts | 4 +- .../adapters/secondaries/time-converter.ts | 21 +++++ .../matcher/domain/dtos/match.request.ts | 22 +++-- .../domain/entities/ecosystem/actor.ts | 8 +- .../entities/ecosystem/{person.ts => ad.ts} | 24 ++--- .../domain/entities/ecosystem/geography.ts | 14 +-- .../matcher/domain/entities/ecosystem/time.ts | 14 ++- .../domain/entities/engine/candidate.ts | 4 +- .../domain/interfaces/ad-request.interface.ts | 3 + .../interfaces/person-request.interface.ts | 3 - .../interfaces/time-converter.interface.ts | 3 + .../interfaces/time-request.interface.ts | 1 + .../matcher/domain/types/actor.type..ts | 4 +- .../domain/types/default-params.type.ts | 2 +- src/modules/matcher/domain/types/mode.enum.ts | 5 + .../domain/types/time-schedule.type.ts | 9 ++ src/modules/matcher/queries/match.query.ts | 54 ++++++----- .../default-params.provider.spec.ts | 2 +- .../unit/domain/ecosystem/geography.spec.ts | 28 +++--- .../unit/domain/ecosystem/person.spec.ts | 42 ++++----- .../engine/algorithm-factory-creator.spec.ts | 2 +- .../engine/algorithm-factory.abstract.spec.ts | 2 +- .../engine/classic-algorithm-factory.spec.ts | 2 +- .../classic-geo.filter.processor.spec.ts | 2 +- .../classic-time.filter.processor.spec.ts | 2 +- ...assic-waypoint.completer.processor.spec.ts | 2 +- .../domain/engine/classic.selector.spec.ts | 2 +- .../domain/engine/completer.abstract.spec.ts | 2 +- .../domain/engine/filter.abstract.spec.ts | 2 +- .../journey.completer.processor.spec.ts | 2 +- .../tests/unit/domain/engine/matcher.spec.ts | 2 +- .../domain/engine/processor.abstract.spec.ts | 2 +- .../engine/route.completer.processor.spec.ts | 2 +- .../domain/engine/selector.abstract.spec.ts | 2 +- .../tests/unit/domain/match.usecase.spec.ts | 2 +- .../tests/unit/queries/match.query.spec.ts | 35 ++++++- 47 files changed, 489 insertions(+), 204 deletions(-) rename src/modules/ad/{ad.modules.ts => ad.module.ts} (100%) create mode 100644 src/modules/ad/tests/unit/adapters/secondaries/messager.spec.ts create mode 100644 src/modules/ad/tests/unit/adapters/secondaries/timezone-finder.spec.ts create mode 100644 src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts create mode 100644 src/modules/ad/tests/unit/domain/time-converter.spec.ts create mode 100644 src/modules/matcher/adapters/secondaries/time-converter.ts rename src/modules/matcher/domain/entities/ecosystem/{person.ts => ad.ts} (54%) create mode 100644 src/modules/matcher/domain/interfaces/ad-request.interface.ts delete mode 100644 src/modules/matcher/domain/interfaces/person-request.interface.ts create mode 100644 src/modules/matcher/domain/interfaces/time-converter.interface.ts create mode 100644 src/modules/matcher/domain/types/mode.enum.ts create mode 100644 src/modules/matcher/domain/types/time-schedule.type.ts diff --git a/src/app.module.ts b/src/app.module.ts index 8236fec..30da99c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +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.modules'; +import { AdModule } from './modules/ad/ad.module'; @Module({ imports: [ diff --git a/src/modules/ad/ad.modules.ts b/src/modules/ad/ad.module.ts similarity index 100% rename from src/modules/ad/ad.modules.ts rename to src/modules/ad/ad.module.ts diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index 02fcd41..a540458 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -43,37 +43,37 @@ export class CreateAdRequest { @IsOptional() @IsString() @AutoMap() - monTime: string | null; + monTime?: string | null; @IsOptional() @IsString() @AutoMap() - tueTime: string | null; + tueTime?: string | null; @IsOptional() @IsString() @AutoMap() - wedTime: string | null; + wedTime?: string | null; @IsOptional() @IsString() @AutoMap() - thuTime!: string | null; + thuTime?: string | null; @IsOptional() @IsString() @AutoMap() - friTime: string | null; + friTime?: string | null; @IsOptional() @IsString() @AutoMap() - satTime: string | null; + satTime?: string | null; @IsOptional() @IsString() @AutoMap() - sunTime: string | null; + sunTime?: string | null; @IsNumber() @AutoMap() @@ -127,7 +127,7 @@ export class CreateAdRequest { @IsOptional() @IsNumber() @AutoMap() - seatsUsed: number; + seatsUsed?: number; @IsString() @AutoMap() @@ -137,5 +137,5 @@ export class CreateAdRequest { @AutoMap() updatedAt: string; - timezone: string; + timezone?: string; } diff --git a/src/modules/ad/domain/entities/time-converter.ts b/src/modules/ad/domain/entities/time-converter.ts index b5910e6..bc0418a 100644 --- a/src/modules/ad/domain/entities/time-converter.ts +++ b/src/modules/ad/domain/entities/time-converter.ts @@ -4,13 +4,16 @@ export class TimeConverter { static toUtcDatetime = ( date: string, time: string, - ianaTimezone: string, - ): Date => - date && time - ? new Date( - new DateTime(`${date}T${time}:00`, TimeZone.zone(ianaTimezone, false)) - .convert(TimeZone.zone('UTC')) - .toIsoString(), - ) - : undefined; + timezone: string, + ): Date => { + try { + return new Date( + new DateTime(`${date}T${time}:00`, TimeZone.zone(timezone, false)) + .convert(TimeZone.zone('UTC')) + .toIsoString(), + ); + } catch (e) { + return undefined; + } + }; } diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index 6cf8d8a..c0b61f2 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -55,72 +55,44 @@ export class AdProfile extends AutomapperProfile { ), forMember( (dest) => dest.monTime, - mapFrom((source) => - TimeConverter.toUtcDatetime( - source.fromDate, - source.monTime, - source.timezone, - ), + mapFrom(({ monTime: time, fromDate: date, timezone }) => + TimeConverter.toUtcDatetime(date, time, timezone), ), ), forMember( (dest) => dest.tueTime, - mapFrom((source) => - TimeConverter.toUtcDatetime( - source.fromDate, - source.tueTime, - source.timezone, - ), + mapFrom(({ tueTime: time, fromDate: date, timezone }) => + TimeConverter.toUtcDatetime(date, time, timezone), ), ), forMember( (dest) => dest.wedTime, - mapFrom((source) => - TimeConverter.toUtcDatetime( - source.fromDate, - source.wedTime, - source.timezone, - ), + mapFrom(({ wedTime: time, fromDate: date, timezone }) => + TimeConverter.toUtcDatetime(date, time, timezone), ), ), forMember( (dest) => dest.thuTime, - mapFrom((source) => - TimeConverter.toUtcDatetime( - source.fromDate, - source.thuTime, - source.timezone, - ), + mapFrom(({ thuTime: time, fromDate: date, timezone }) => + TimeConverter.toUtcDatetime(date, time, timezone), ), ), forMember( (dest) => dest.friTime, - mapFrom((source) => - TimeConverter.toUtcDatetime( - source.fromDate, - source.friTime, - source.timezone, - ), + mapFrom(({ friTime: time, fromDate: date, timezone }) => + TimeConverter.toUtcDatetime(date, time, timezone), ), ), forMember( (dest) => dest.satTime, - mapFrom((source) => - TimeConverter.toUtcDatetime( - source.fromDate, - source.satTime, - source.timezone, - ), + mapFrom(({ satTime: time, fromDate: date, timezone }) => + TimeConverter.toUtcDatetime(date, time, timezone), ), ), forMember( (dest) => dest.sunTime, - mapFrom((source) => - TimeConverter.toUtcDatetime( - source.fromDate, - source.sunTime, - source.timezone, - ), + mapFrom(({ sunTime: time, fromDate: date, timezone }) => + TimeConverter.toUtcDatetime(date, time, timezone), ), ), ); 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..c879974 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts @@ -0,0 +1,91 @@ +import { CreateAdRequest } from '../../../domain/dtos/create-ad.request'; +import { PointType } from '../../../../geography/domain/types/point-type.enum'; +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'; + +const mockAdRepository = {}; + +const createAdRequest: CreateAdRequest = { + uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1', + driver: true, + passenger: false, + frequency: 2, + fromDate: '2023-04-26', + toDate: '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, + originType: PointType.OTHER, + destinationType: PointType.OTHER, + seatsDriver: 3, + seatsPassenger: 1, + createdAt: '2023-04-01 12:45', + updatedAt: '2023-04-01 12:45', + waypoints: [ + { lon: 6, lat: 45 }, + { lon: 6.5, lat: 45.5 }, + ], +}; + +describe('CreateAdUseCase', () => { + let createAdUseCase: CreateAdUseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AdRepository, + useValue: mockAdRepository, + }, + AdProfile, + CreateAdUseCase, + ], + }).compile(); + + createAdUseCase = module.get(CreateAdUseCase); + }); + + it('should be defined', () => { + expect(createAdUseCase).toBeDefined(); + }); + + describe('execute', () => { + it('should create an ad', async () => { + const ad = await createAdUseCase.execute( + new CreateAdCommand(createAdRequest), + ); + expect(ad).toBeInstanceOf(Ad); + }); + + // it('should throw an exception when error occurs', async () => { + // await expect( + // createAdUseCase.execute( + // new MatchQuery( + // matchRequest, + // defaultParams, + // mockGeorouterCreator, + // mockTimezoneFinder, + // ), + // ), + // ).rejects.toBeInstanceOf(MatcherException); + // }); + }); +}); 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..5539cd9 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/time-converter.spec.ts @@ -0,0 +1,35 @@ +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( + '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, time or timezone', () => { + expect( + TimeConverter.toUtcDatetime(undefined, '07:00', 'Europe/Paris'), + ).toBeUndefined(); + expect( + TimeConverter.toUtcDatetime('2023-13-01', '07:00', 'Europe/Paris'), + ).toBeUndefined(); + expect( + TimeConverter.toUtcDatetime('2023-05-01', undefined, 'Europe/Paris'), + ).toBeUndefined(); + expect( + TimeConverter.toUtcDatetime('2023-05-01', 'a', 'Europe/Paris'), + ).toBeUndefined(); + expect( + TimeConverter.toUtcDatetime('2023-13-01', '07:00', 'OlympusMons/Mars'), + ).toBeUndefined(); + }); +}); diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 22b3b4a..76c0f9d 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -12,6 +12,7 @@ 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({ @@ -27,6 +28,7 @@ export class MatcherController { @InjectMapper() private readonly _mapper: Mapper, private readonly georouterCreator: GeorouterCreator, private readonly timezoneFinder: GeoTimezoneFinder, + private readonly timeConverter: TimeConverter, ) {} @GrpcMethod('MatcherService', 'Match') @@ -38,6 +40,7 @@ export class MatcherController { this.defaultParamsProvider.getParams(), this.georouterCreator, this.timezoneFinder, + this.timeConverter, ), ); return Promise.resolve({ diff --git a/src/modules/matcher/adapters/primaries/matcher.proto b/src/modules/matcher/adapters/primaries/matcher.proto index af4e083..f18aaff 100644 --- a/src/modules/matcher/adapters/primaries/matcher.proto +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -7,31 +7,32 @@ 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; + Mode mode = 1; + string uuid = 2; + repeated Coordinates waypoints = 3; + string departure = 4; + string fromDate = 5; + Schedule schedule = 6; + bool driver = 7; + bool passenger = 8; + string toDate = 9; + int32 marginDuration = 10; + MarginDurations marginDurations = 11; + int32 seatsPassenger = 12; + int32 seatsDriver = 13; + bool strict = 14; + Algorithm algorithm = 15; + int32 remoteness = 16; + bool useProportion = 17; + int32 proportion = 18; + bool useAzimuth = 19; + int32 azimuthMargin = 20; + float maxDetourDistanceRatio = 21; + float maxDetourDurationRatio = 22; + repeated int32 exclusions = 23; } -message Point { +message Coordinates { float lon = 1; float lat = 2; } @@ -68,3 +69,9 @@ message Matches { repeated Match data = 1; int32 total = 2; } + +enum Mode { + MATCH = 0; + PUBLISH = 1; + PUBLISH_AND_MATCH = 2; +} diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index c67dc10..5bb3158 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -8,9 +8,7 @@ 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'), 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/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 5ffea23..bcd1824 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -15,19 +15,30 @@ 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: Point[]; @@ -138,10 +149,7 @@ export class MatchRequest @IsOptional() @IsArray() - exclusions: number[]; + 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/person.ts b/src/modules/matcher/domain/entities/ecosystem/ad.ts similarity index 54% rename from src/modules/matcher/domain/entities/ecosystem/person.ts rename to src/modules/matcher/domain/entities/ecosystem/ad.ts index 2f0906c..5046579 100644 --- a/src/modules/matcher/domain/entities/ecosystem/person.ts +++ b/src/modules/matcher/domain/entities/ecosystem/ad.ts @@ -1,24 +1,24 @@ -import { IRequestPerson } from '../../interfaces/person-request.interface'; +import { IRequestAd } from '../../interfaces/ad-request.interface'; -export class Person { - private personRequest: IRequestPerson; - private defaultIdentifier: number; +export class Ad { + private adRequest: IRequestAd; + private defaultUuid: string; private defaultMarginDuration: number; - identifier: number; + uuid: string; marginDurations: number[]; constructor( - personRequest: IRequestPerson, - defaultIdentifier: number, + adRequest: IRequestAd, + defaultUuid: string, defaultMarginDuration: number, ) { - this.personRequest = personRequest; - this.defaultIdentifier = defaultIdentifier; + this.adRequest = adRequest; + this.defaultUuid = defaultUuid; this.defaultMarginDuration = defaultMarginDuration; } init = (): void => { - this.setIdentifier(this.personRequest.identifier ?? this.defaultIdentifier); + this.setUuid(this.adRequest.uuid ?? this.defaultUuid); this.setMarginDurations([ this.defaultMarginDuration, this.defaultMarginDuration, @@ -30,8 +30,8 @@ export class Person { ]); }; - setIdentifier = (identifier: number): void => { - this.identifier = identifier; + setUuid = (uuid: string): void => { + this.uuid = uuid; }; setMarginDurations = (marginDurations: number[]): void => { diff --git a/src/modules/matcher/domain/entities/ecosystem/geography.ts b/src/modules/matcher/domain/entities/ecosystem/geography.ts index 68d1a39..9afd432 100644 --- a/src/modules/matcher/domain/entities/ecosystem/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -10,7 +10,7 @@ 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'; @@ -18,7 +18,7 @@ import { Timezoner } from './timezoner'; export class Geography { private geographyRequest: IRequestGeography; - private person: Person; + private ad: Ad; private points: Point[]; originType: PointType; destinationType: PointType; @@ -30,10 +30,10 @@ export class Geography { constructor( geographyRequest: IRequestGeography, timezoner: Timezoner, - person: Person, + ad: Ad, ) { this.geographyRequest = geographyRequest; - this.person = person; + this.ad = ad; this.points = []; this.originType = undefined; this.destinationType = undefined; @@ -178,11 +178,11 @@ export class Geography { 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/time.ts b/src/modules/matcher/domain/entities/ecosystem/time.ts index 6059784..d49df7b 100644 --- a/src/modules/matcher/domain/entities/ecosystem/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -5,26 +5,30 @@ import { import { MarginDurations } from '../../types/margin-durations.type'; import { IRequestTime } from '../../interfaces/time-request.interface'; import { DAYS } from '../../types/days.const'; -import { Schedule } from '../../types/schedule.type'; +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 defaultValidityDuration: number; + 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.defaultValidityDuration = defaultValidityDuration; + this.timeConverter = timeConverter; this.schedule = {}; this.marginDurations = { mon: defaultMarginDuration, @@ -128,8 +132,10 @@ export class Time { private setPunctualRequest = (): void => { if (this.timeRequest.departure) { this.frequency = Frequency.PUNCTUAL; - this.schedule[Day[this.fromDate.getDay()]] = - this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); + this.schedule[Day[this.fromDate.getDay()]] = this.timeConverter.toUtcDate( + this.fromDate, + this.timeRequest.timezone, + ); } }; 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/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/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/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/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/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 7de0009..b8c09b5 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'; @@ -9,35 +9,42 @@ 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; + mode: Mode; + ad: Ad; roles: Role[]; time: Time; geography: Geography; - exclusions: number[]; + 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.timezoneFinder = timezoneFinder; - this.setPerson(); + this.timeConverter = timeConverter; + this.setMode(); + this.setAd(); this.setRoles(); - this.setTime(); this.setGeography(); + this.setTime(); this.setRequirement(); this.setAlgorithmSettings(); this.setExclusions(); @@ -47,13 +54,17 @@ export class MatchQuery { this.geography.createRoutes(this.roles, this.algorithmSettings.georouter); }; - private setPerson = (): void => { - this.person = new Person( + private setMode = (): void => { + this.mode = this.matchRequest.mode ?? Mode.MATCH; + }; + + private setAd = (): void => { + this.ad = new Ad( this.matchRequest, - this.defaultParams.DEFAULT_IDENTIFIER, + this.defaultParams.DEFAULT_UUID, this.defaultParams.MARGIN_DURATION, ); - this.person.init(); + this.ad.init(); }; private setRoles = (): void => { @@ -63,15 +74,6 @@ export class MatchQuery { if (this.roles.length == 0) this.roles.push(Role.PASSENGER); }; - private setTime = (): void => { - this.time = new Time( - this.matchRequest, - this.defaultParams.MARGIN_DURATION, - this.defaultParams.VALIDITY_DURATION, - ); - this.time.init(); - }; - private setGeography = (): void => { this.geography = new Geography( this.matchRequest, @@ -79,9 +81,20 @@ export class MatchQuery { timezone: this.defaultParams.DEFAULT_TIMEZONE, finder: this.timezoneFinder, }, - this.person, + 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.time.init(); }; private setRequirement = (): void => { @@ -102,8 +115,7 @@ export class MatchQuery { private setExclusions = (): void => { this.exclusions = []; - if (this.matchRequest.identifier) - this.exclusions.push(this.matchRequest.identifier); + 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/domain/ecosystem/geography.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts index 9b870e8..1ca35d5 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -1,4 +1,4 @@ -import { Person } from '../../../../domain/entities/ecosystem/person'; +import { Ad } from '../../../../domain/entities/ecosystem/ad'; import { Geography, RouteKey, @@ -9,11 +9,11 @@ import { Route } from '../../../../domain/entities/ecosystem/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, ); @@ -88,7 +88,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); expect(geography).toBeDefined(); }); @@ -114,7 +114,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); geography.init(); expect(geography.originType).toBe(PointType.LOCALITY); @@ -129,7 +129,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -147,7 +147,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -169,7 +169,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -191,7 +191,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); expect(() => geography.init()).toThrow(); }); @@ -216,7 +216,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); geography.init(); await geography.createRoutes( @@ -249,7 +249,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); geography.init(); await geography.createRoutes( @@ -278,7 +278,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); geography.init(); await geography.createRoutes([Role.DRIVER], mockGeorouter); @@ -304,7 +304,7 @@ describe('Geography entity', () => { timezone: 'Europe/Paris', finder: mockTimezoneFinder, }, - person, + ad, ); geography.init(); await geography.createRoutes([Role.PASSENGER], mockGeorouter); 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/engine/algorithm-factory-creator.spec.ts b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts index 1a816ee..daa0dce 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 f17f5a4..7b5d05f 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 @@ -16,7 +16,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 690fee3..9d76d87 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 fda1087..10ac6ad 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 6c703af..54791a5 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 9a52ce4..5b25396 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 77d616b..a15df9c 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 15fad37..913b309 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 6b96578..de67dc5 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 eb79edf..f31b8e5 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 1240682..bb7446c 100644 --- a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts @@ -26,7 +26,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 65f6a00..6665000 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 7076539..311c42d 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 d8830cb..85302dc 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 @@ -14,7 +14,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 1ca097b..d80dd1f 100644 --- a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -39,7 +39,7 @@ const mockTimezoneFinder = { }; const defaultParams: IDefaultParams = { - DEFAULT_IDENTIFIER: 0, + DEFAULT_UUID: '00000000-0000-0000-0000-000000000000', MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', 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 7640929..be1bfb5 100644 --- a/src/modules/matcher/tests/unit/queries/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -4,9 +4,10 @@ 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', @@ -55,6 +56,30 @@ describe('Match query', () => { mockTimezoneFinder, ); 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, + ); + expect(matchQuery.mode).toBe(Mode.PUBLISH_AND_MATCH); }); it('should create a query with excluded identifiers', () => { @@ -70,8 +95,12 @@ 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, From 0d407216bcc2eb7ba5f5e1280b993809c75b6c8b Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 3 May 2023 10:57:27 +0200 Subject: [PATCH 10/19] add time converter to match module --- .../matcher/domain/entities/ecosystem/time.ts | 9 ++++++- src/modules/matcher/queries/match.query.ts | 1 + .../unit/domain/ecosystem/geography.spec.ts | 2 +- .../tests/unit/domain/ecosystem/time.spec.ts | 24 ++++++++++++++++++- .../engine/algorithm-factory-creator.spec.ts | 7 +++++- .../engine/algorithm-factory.abstract.spec.ts | 7 +++++- .../engine/classic-algorithm-factory.spec.ts | 7 +++++- .../classic-geo.filter.processor.spec.ts | 7 +++++- .../classic-time.filter.processor.spec.ts | 7 +++++- ...assic-waypoint.completer.processor.spec.ts | 7 +++++- .../domain/engine/classic.selector.spec.ts | 7 +++++- .../domain/engine/completer.abstract.spec.ts | 7 +++++- .../domain/engine/filter.abstract.spec.ts | 7 +++++- .../journey.completer.processor.spec.ts | 7 +++++- .../tests/unit/domain/engine/matcher.spec.ts | 7 +++++- .../domain/engine/processor.abstract.spec.ts | 7 +++++- .../engine/route.completer.processor.spec.ts | 7 +++++- .../domain/engine/selector.abstract.spec.ts | 7 +++++- .../tests/unit/domain/match.usecase.spec.ts | 8 ++++++- .../tests/unit/queries/match.query.spec.ts | 14 ++++++++++- 20 files changed, 137 insertions(+), 19 deletions(-) diff --git a/src/modules/matcher/domain/entities/ecosystem/time.ts b/src/modules/matcher/domain/entities/ecosystem/time.ts index d49df7b..83e3415 100644 --- a/src/modules/matcher/domain/entities/ecosystem/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -151,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, + ); }); }; diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index b8c09b5..8ac0388 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -93,6 +93,7 @@ export class MatchQuery { this.matchRequest, this.defaultParams.MARGIN_DURATION, this.defaultParams.VALIDITY_DURATION, + this.timeConverter, ); this.time.init(); }; 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 1ca35d5..82925c7 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -66,7 +66,7 @@ const mockGeorouter = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), }; describe('Geography entity', () => { 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 daa0dce..e9c076b 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 7b5d05f..f4b62c2 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 @@ -12,7 +12,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -53,6 +57,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 9d76d87..070b8ee 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 10ac6ad..a6272d6 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 54791a5..825a186 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 5b25396..961db3f 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 a15df9c..d20cd56 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 913b309..3dc02f4 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 de67dc5..09fef45 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 f31b8e5..5f6c135 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 bb7446c..c4ce2fd 100644 --- a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts @@ -22,7 +22,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -63,6 +67,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 6665000..34948a6 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn().mockImplementation(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 311c42d..a1ac083 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 85302dc..a681135 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 @@ -10,7 +10,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -51,6 +55,7 @@ const matchQuery: MatchQuery = new MatchQuery( 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 d80dd1f..10d97aa 100644 --- a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -35,7 +35,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; const defaultParams: IDefaultParams = { @@ -106,6 +110,7 @@ describe('MatchUseCase', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ), ); expect(matches.total).toBe(3); @@ -119,6 +124,7 @@ describe('MatchUseCase', () => { 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 be1bfb5..bc14991 100644 --- a/src/modules/matcher/tests/unit/queries/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -32,7 +32,11 @@ const mockGeorouterCreator = { }; const mockTimezoneFinder = { - timezones: jest.fn().mockImplementation(), + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter = { + toUtcDate: jest.fn(), }; describe('Match query', () => { @@ -54,6 +58,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery).toBeDefined(); expect(matchQuery.mode).toBe(Mode.MATCH); @@ -78,6 +83,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.mode).toBe(Mode.PUBLISH_AND_MATCH); }); @@ -106,6 +112,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.exclusions.length).toBe(4); }); @@ -129,6 +136,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.roles).toEqual([Role.DRIVER]); }); @@ -152,6 +160,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.roles).toEqual([Role.PASSENGER]); }); @@ -176,6 +185,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.roles.length).toBe(2); expect(matchQuery.roles).toContain(Role.PASSENGER); @@ -202,6 +212,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.requirement.seatsDriver).toBe(1); expect(matchQuery.requirement.seatsPassenger).toBe(2); @@ -234,6 +245,7 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, mockTimezoneFinder, + mockTimeConverter, ); expect(matchQuery.algorithmSettings.algorithmType).toBe( AlgorithmType.CLASSIC, From 7444eabf3d089abeda9929a9e324c164388f942f Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 3 May 2023 13:32:49 +0200 Subject: [PATCH 11/19] change migration for postgis --- prisma/migrations/20230425130853_/migration.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/migrations/20230425130853_/migration.sql b/prisma/migrations/20230425130853_/migration.sql index 8c7b5e6..a0514d2 100644 --- a/prisma/migrations/20230425130853_/migration.sql +++ b/prisma/migrations/20230425130853_/migration.sql @@ -2,8 +2,8 @@ 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; +-- set the search_path to both public (where is postgis) AND the current schema +SET search_path TO matcher, public; -- CreateTable CREATE TABLE "ad" ( From 2a2cfa5c0f72c27d0dd4e812ba0a2ff0ab0b94fe Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 5 May 2023 08:56:25 +0200 Subject: [PATCH 12/19] wip --- src/modules/ad/adapters/secondaries/ad.repository.ts | 4 ++-- .../adapters/secondaries/prisma-repository.abstract.ts | 0 .../{src => }/adapters/secondaries/prisma-service.ts | 0 src/modules/database/database.module.ts | 2 +- src/modules/database/{src => }/domain/matcher-repository.ts | 0 src/modules/database/{src => }/domain/point.type.ts | 0 .../database/{src => }/exceptions/database.exception.ts | 0 .../database/{src => }/interfaces/collection.interface.ts | 0 .../database/{src => }/interfaces/repository.interface.ts | 0 src/modules/database/tests/unit/prisma-repository.spec.ts | 6 +++--- .../matcher/adapters/primaries/matcher.controller.ts | 4 ++-- 11 files changed, 8 insertions(+), 8 deletions(-) rename src/modules/database/{src => }/adapters/secondaries/prisma-repository.abstract.ts (100%) rename src/modules/database/{src => }/adapters/secondaries/prisma-service.ts (100%) rename src/modules/database/{src => }/domain/matcher-repository.ts (100%) rename src/modules/database/{src => }/domain/point.type.ts (100%) rename src/modules/database/{src => }/exceptions/database.exception.ts (100%) rename src/modules/database/{src => }/interfaces/collection.interface.ts (100%) rename src/modules/database/{src => }/interfaces/repository.interface.ts (100%) diff --git a/src/modules/ad/adapters/secondaries/ad.repository.ts b/src/modules/ad/adapters/secondaries/ad.repository.ts index b100584..a57910a 100644 --- a/src/modules/ad/adapters/secondaries/ad.repository.ts +++ b/src/modules/ad/adapters/secondaries/ad.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { MatcherRepository } from '../../../database/src/domain/matcher-repository'; +import { MatcherRepository } from '../../../database/domain/matcher-repository'; import { Ad } from '../../domain/entities/ad'; -import { DatabaseException } from '../../../database/src/exceptions/database.exception'; +import { DatabaseException } from '../../../database/exceptions/database.exception'; @Injectable() export class AdRepository extends MatcherRepository { 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 100% rename from src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts rename to src/modules/database/adapters/secondaries/prisma-repository.abstract.ts 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 2d18636..5b09ac3 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { PrismaService } from './src/adapters/secondaries/prisma-service'; +import { PrismaService } from './adapters/secondaries/prisma-service'; import { AdRepository } from '../ad/adapters/secondaries/ad.repository'; @Module({ 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..984c69d 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 { diff --git a/src/modules/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 76c0f9d..0bbabbf 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -3,9 +3,9 @@ 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'; From da96f52c1e292079f4066ea7e40ba595fbffbcf6 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 11 May 2023 17:47:55 +0200 Subject: [PATCH 13/19] wip --- .../migrations/20230425130853_/migration.sql | 65 --- prisma/migrations/migration_lock.toml | 3 - prisma/schema.prisma | 35 +- src/modules/ad/ad.module.ts | 8 + .../primaries/ad-messager.controller.ts | 26 +- .../ad/adapters/secondaries/ad.repository.ts | 5 +- .../secondaries/default-params.provider.ts | 16 + .../ad/adapters/secondaries/messager.ts | 4 +- src/modules/ad/commands/create-ad.command.ts | 47 +- .../ad/domain/dtos/create-ad.request.ts | 86 ++-- .../ad/domain/entities/ad.completer.ts | 7 + src/modules/ad/domain/entities/ad.ts | 21 +- src/modules/ad/domain/entities/geography.ts | 98 ++++ .../ad/domain/entities/time-converter.ts | 12 +- .../ad/domain/types/default-params.type.ts | 5 + src/modules/ad/domain/types/frequency.enum.ts | 4 +- src/modules/ad/domain/types/role.enum.ts | 4 + .../ad/domain/usecases/create-ad.usecase.ts | 10 + src/modules/ad/mappers/ad.profile.ts | 34 -- .../default-params.provider.spec.ts | 38 ++ .../ad/tests/unit/domain/ad.completer.spec.ts | 42 ++ .../unit/domain/create-ad.usecase.spec.ts | 30 +- .../tests/unit/domain/time-converter.spec.ts | 22 +- .../secondaries/prisma-repository.abstract.ts | 26 +- .../tests/unit/prisma-repository.spec.ts | 2 +- .../adapters/secondaries/georouter-creator.ts | 28 ++ .../secondaries/graphhopper-georouter.ts | 324 +++++++++++++ .../domain/entities}/route.ts | 14 +- .../domain/entities/spacetime-point.ts | 13 + .../interfaces/georouter-creator.interface.ts | 5 + .../domain/interfaces/georouter.interface.ts | 7 + .../domain/types/georouter-settings.type.ts | 5 + .../geography/domain/types/named-route.ts | 6 + .../geography/domain/types/path.type.ts | 6 + .../geography/domain/types/timezoner.ts | 6 + .../exceptions/geography.exception.ts | 13 + .../tests/unit/georouter-creator.spec.ts | 47 ++ .../tests/unit/graphhopper-georouter.spec.ts | 456 ++++++++++++++++++ .../geography/tests/unit/route.spec.ts | 48 ++ .../adapters/primaries/matcher.controller.ts | 4 +- .../matcher/adapters/primaries/matcher.proto | 51 +- .../secondaries/default-params.provider.ts | 22 +- .../secondaries/graphhopper-georouter.ts | 6 +- .../entities/ecosystem/algorithm-settings.ts | 22 +- .../domain/entities/ecosystem/geography.ts | 6 +- .../entities/ecosystem/matcher-route.ts | 16 + .../domain/entities/ecosystem/named-route.ts | 4 +- .../types/default-algorithm-settings.type.ts | 22 +- .../matcher/domain/usecases/match.usecase.ts | 14 +- src/modules/matcher/matcher.module.ts | 2 + .../unit/domain/ecosystem/geography.spec.ts | 12 +- .../{route.spec.ts => matcher-route.spec.ts} | 12 +- .../engine/algorithm-factory-creator.spec.ts | 22 +- .../engine/algorithm-factory.abstract.spec.ts | 22 +- .../engine/classic-algorithm-factory.spec.ts | 22 +- .../classic-geo.filter.processor.spec.ts | 22 +- .../classic-time.filter.processor.spec.ts | 22 +- ...assic-waypoint.completer.processor.spec.ts | 22 +- .../domain/engine/classic.selector.spec.ts | 22 +- .../domain/engine/completer.abstract.spec.ts | 22 +- .../domain/engine/filter.abstract.spec.ts | 22 +- .../journey.completer.processor.spec.ts | 22 +- .../tests/unit/domain/engine/matcher.spec.ts | 22 +- .../domain/engine/processor.abstract.spec.ts | 22 +- .../engine/route.completer.processor.spec.ts | 22 +- .../domain/engine/selector.abstract.spec.ts | 22 +- .../tests/unit/domain/match.usecase.spec.ts | 22 +- .../tests/unit/queries/match.query.spec.ts | 22 +- src/modules/utils/exception-code.enum.ts | 19 + 69 files changed, 1700 insertions(+), 492 deletions(-) delete mode 100644 prisma/migrations/20230425130853_/migration.sql delete mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/modules/ad/adapters/secondaries/default-params.provider.ts create mode 100644 src/modules/ad/domain/entities/ad.completer.ts create mode 100644 src/modules/ad/domain/entities/geography.ts create mode 100644 src/modules/ad/domain/types/default-params.type.ts create mode 100644 src/modules/ad/domain/types/role.enum.ts create mode 100644 src/modules/ad/tests/unit/adapters/secondaries/default-params.provider.spec.ts create mode 100644 src/modules/ad/tests/unit/domain/ad.completer.spec.ts create mode 100644 src/modules/geography/adapters/secondaries/georouter-creator.ts create mode 100644 src/modules/geography/adapters/secondaries/graphhopper-georouter.ts rename src/modules/{matcher/domain/entities/ecosystem => geography/domain/entities}/route.ts (72%) create mode 100644 src/modules/geography/domain/entities/spacetime-point.ts create mode 100644 src/modules/geography/domain/interfaces/georouter-creator.interface.ts create mode 100644 src/modules/geography/domain/interfaces/georouter.interface.ts create mode 100644 src/modules/geography/domain/types/georouter-settings.type.ts create mode 100644 src/modules/geography/domain/types/named-route.ts create mode 100644 src/modules/geography/domain/types/path.type.ts create mode 100644 src/modules/geography/domain/types/timezoner.ts create mode 100644 src/modules/geography/exceptions/geography.exception.ts create mode 100644 src/modules/geography/tests/unit/georouter-creator.spec.ts create mode 100644 src/modules/geography/tests/unit/graphhopper-georouter.spec.ts create mode 100644 src/modules/geography/tests/unit/route.spec.ts create mode 100644 src/modules/matcher/domain/entities/ecosystem/matcher-route.ts rename src/modules/matcher/tests/unit/domain/ecosystem/{route.spec.ts => matcher-route.spec.ts} (84%) create mode 100644 src/modules/utils/exception-code.enum.ts diff --git a/prisma/migrations/20230425130853_/migration.sql b/prisma/migrations/20230425130853_/migration.sql deleted file mode 100644 index a0514d2..0000000 --- a/prisma/migrations/20230425130853_/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 (where is postgis) AND the current schema -SET search_path TO matcher, public; - --- CreateTable -CREATE TABLE "ad" ( - "uuid" UUID NOT NULL, - "driver" BOOLEAN NOT NULL, - "passenger" BOOLEAN NOT NULL, - "frequency" INTEGER NOT NULL, - "fromDate" DATE NOT NULL, - "toDate" DATE NOT NULL, - "monTime" TIMESTAMPTZ NOT NULL, - "tueTime" TIMESTAMPTZ NOT NULL, - "wedTime" TIMESTAMPTZ NOT NULL, - "thuTime" TIMESTAMPTZ NOT NULL, - "friTime" TIMESTAMPTZ NOT NULL, - "satTime" TIMESTAMPTZ NOT NULL, - "sunTime" TIMESTAMPTZ NOT NULL, - "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 NOT NULL, - "driverDistance" INTEGER NOT NULL, - "passengerDuration" INTEGER NOT NULL, - "passengerDistance" INTEGER NOT NULL, - "originType" SMALLINT NOT NULL, - "destinationType" SMALLINT NOT NULL, - "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, - "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/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 469a604..2086694 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,19 +13,20 @@ datasource db { } model Ad { - uuid String @id @default(uuid()) @db.Uuid + uuid String @id @db.Uuid + userUuid String @db.Uuid driver Boolean passenger Boolean - frequency Int + 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() + 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 @@ -33,12 +34,10 @@ model Ad { friMargin Int satMargin Int sunMargin Int - driverDuration Int - driverDistance Int - passengerDuration Int - passengerDistance Int - originType Int @db.SmallInt - destinationType Int @db.SmallInt + driverDuration Int? + driverDistance Int? + passengerDuration Int? + passengerDistance Int? waypoints Unsupported("geography(LINESTRING)")? direction Unsupported("geography(LINESTRING)")? fwdAzimuth Int @@ -46,6 +45,7 @@ model Ad { seatsDriver Int @db.SmallInt seatsPassenger Int @db.SmallInt seatsUsed Int @db.SmallInt + strict Boolean createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -57,3 +57,8 @@ model Ad { @@index([direction], name: "direction_idx", type: Gist) @@map("ad") } + +enum Frequency { + PUNCTUAL + RECURRENT +} diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 91717d5..bc23450 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -10,11 +10,17 @@ import { CqrsModule } from '@nestjs/cqrs'; import { Messager } from './adapters/secondaries/messager'; import { TimezoneFinder } from './adapters/secondaries/timezone-finder'; 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'; @Module({ imports: [ + GeographyModule, DatabaseModule, CqrsModule, + HttpModule, RabbitMQModule.forRootAsync(RabbitMQModule, { imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ @@ -46,6 +52,8 @@ import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezon TimezoneFinder, GeoTimezoneFinder, CreateAdUseCase, + DefaultParamsProvider, + GeorouterCreator, ], exports: [], }) diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts index 4028b69..c122475 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -8,7 +8,10 @@ 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 { GeoTimezoneFinder } from 'src/modules/geography/adapters/secondaries/geo-timezone-finder'; +import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder'; +import { plainToInstance } from 'class-transformer'; +import { DefaultParamsProvider } from '../secondaries/default-params.provider'; +import { GeorouterCreator } from '../../../geography/adapters/secondaries/georouter-creator'; @Controller() export class AdMessagerController { @@ -16,6 +19,8 @@ export class AdMessagerController { private readonly messager: Messager, private readonly commandBus: CommandBus, @InjectMapper() private readonly mapper: Mapper, + private readonly defaultParamsProvider: DefaultParamsProvider, + private readonly georouterCreator: GeorouterCreator, private readonly timezoneFinder: GeoTimezoneFinder, ) {} @@ -23,15 +28,10 @@ export class AdMessagerController { name: 'adCreated', }) async adCreatedHandler(message: string): Promise { - let createAdRequest: CreateAdRequest; try { - // parse message to conform to CreateAdRequest (not a real instance yet) - const parsedMessage: CreateAdRequest = JSON.parse(message); - // create a real instance of CreateAdRequest from parsed message - createAdRequest = this.mapper.map( - parsedMessage, - CreateAdRequest, + const createAdRequest: CreateAdRequest = plainToInstance( CreateAdRequest, + JSON.parse(message), ); // validate instance await validateOrReject(createAdRequest); @@ -48,14 +48,20 @@ export class AdMessagerController { createAdRequest.waypoints[0].lat, )[0]; const ad: Ad = await this.commandBus.execute( - new CreateAdCommand(createAdRequest), + new CreateAdCommand( + createAdRequest, + this.defaultParamsProvider.getParams(), + this.georouterCreator, + this.timezoneFinder, + ), ); console.log(ad); } catch (e) { + console.log(e); this.messager.publish( 'logging.matcher.ad.crit', JSON.stringify({ - createAdRequest, + message, error: e, }), ); diff --git a/src/modules/ad/adapters/secondaries/ad.repository.ts b/src/modules/ad/adapters/secondaries/ad.repository.ts index a57910a..11aa335 100644 --- a/src/modules/ad/adapters/secondaries/ad.repository.ts +++ b/src/modules/ad/adapters/secondaries/ad.repository.ts @@ -2,10 +2,11 @@ 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'; +import { Frequency } from '../../domain/types/frequency.enum'; @Injectable() export class AdRepository extends MatcherRepository { - protected _model = 'ad'; + protected model = 'ad'; async createAd(ad: Partial): Promise { try { @@ -44,7 +45,7 @@ type AdFields = { uuid: string; driver: string; passenger: string; - frequency: number; + frequency: Frequency; fromDate: string; toDate: string; monTime: 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..62e45aa --- /dev/null +++ b/src/modules/ad/adapters/secondaries/default-params.provider.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IDefaultParams } from '../../domain/types/default-params.type'; + +@Injectable() +export class DefaultParamsProvider { + constructor(private readonly configService: ConfigService) {} + + getParams = (): IDefaultParams => { + 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/messager.ts b/src/modules/ad/adapters/secondaries/messager.ts index 96fa7cc..1888b7d 100644 --- a/src/modules/ad/adapters/secondaries/messager.ts +++ b/src/modules/ad/adapters/secondaries/messager.ts @@ -6,13 +6,13 @@ import { MessageBroker } from './message-broker'; @Injectable() export class Messager extends MessageBroker { constructor( - private readonly _amqpConnection: AmqpConnection, + private readonly amqpConnection: AmqpConnection, configService: ConfigService, ) { super(configService.get('RMQ_EXCHANGE')); } publish = (routingKey: string, message: string): void => { - this._amqpConnection.publish(this.exchange, routingKey, message); + this.amqpConnection.publish(this.exchange, routingKey, message); }; } diff --git a/src/modules/ad/commands/create-ad.command.ts b/src/modules/ad/commands/create-ad.command.ts index b4f1e8d..d1e1d0a 100644 --- a/src/modules/ad/commands/create-ad.command.ts +++ b/src/modules/ad/commands/create-ad.command.ts @@ -1,9 +1,54 @@ +import { IGeorouter } from '../../geography/domain/interfaces/georouter.interface'; +import { ICreateGeorouter } from '../../geography/domain/interfaces/georouter-creator.interface'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; +import { Geography } from '../domain/entities/geography'; +import { IDefaultParams } from '../domain/types/default-params.type'; +import { Role } from '../domain/types/role.enum'; +import { IFindTimezone } from '../../geography/domain/interfaces/timezone-finder.interface'; export class CreateAdCommand { readonly createAdRequest: CreateAdRequest; + private readonly defaultParams: IDefaultParams; + private readonly georouter: IGeorouter; + private readonly timezoneFinder: IFindTimezone; + roles: Role[]; + geography: Geography; + timezone: string; - constructor(request: CreateAdRequest) { + constructor( + request: CreateAdRequest, + defaultParams: IDefaultParams, + georouterCreator: ICreateGeorouter, + timezoneFinder: IFindTimezone, + ) { this.createAdRequest = request; + this.defaultParams = defaultParams; + this.georouter = georouterCreator.create( + defaultParams.GEOROUTER_TYPE, + defaultParams.GEOROUTER_URL, + ); + this.timezoneFinder = timezoneFinder; + this.setRoles(); + this.setGeography(); } + + private setRoles = (): void => { + this.roles = []; + if (this.createAdRequest.driver) this.roles.push(Role.DRIVER); + if (this.createAdRequest.passenger) this.roles.push(Role.PASSENGER); + }; + + private setGeography = async (): Promise => { + this.geography = new Geography(this.createAdRequest.waypoints, { + timezone: this.defaultParams.DEFAULT_TIMEZONE, + finder: this.timezoneFinder, + }); + if (this.geography.timezones.length > 0) + this.createAdRequest.timezone = this.geography.timezones[0]; + try { + await this.geography.createRoutes(this.roles, this.georouter); + } catch (e) { + console.log(e); + } + }; } diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index a540458..7dce0b1 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -3,15 +3,17 @@ import { ArrayMinSize, IsArray, IsBoolean, + IsDate, IsEnum, + IsMilitaryTime, IsNotEmpty, IsNumber, IsOptional, IsString, } from 'class-validator'; -import { PointType } from '../../../geography/domain/types/point-type.enum'; import { Frequency } from '../types/frequency.enum'; import { Coordinates } from '../../../geography/domain/entities/coordinates'; +import { Type } from 'class-transformer'; export class CreateAdRequest { @IsString() @@ -19,6 +21,11 @@ export class CreateAdRequest { @AutoMap() uuid: string; + @IsString() + @IsNotEmpty() + @AutoMap() + userUuid: string; + @IsBoolean() @AutoMap() driver: boolean; @@ -27,53 +34,54 @@ export class CreateAdRequest { @AutoMap() passenger: boolean; - @IsNotEmpty() @IsEnum(Frequency) @AutoMap() frequency: Frequency; - @IsString() + @Type(() => Date) + @IsDate() @AutoMap() - fromDate: string; + fromDate: Date; - @IsString() + @Type(() => Date) + @IsDate() @AutoMap() - toDate: string; + toDate: Date; @IsOptional() - @IsString() + @IsMilitaryTime() @AutoMap() - monTime?: string | null; + monTime?: string; @IsOptional() - @IsString() + @IsMilitaryTime() @AutoMap() - tueTime?: string | null; + tueTime?: string; @IsOptional() - @IsString() + @IsMilitaryTime() @AutoMap() - wedTime?: string | null; + wedTime?: string; @IsOptional() - @IsString() + @IsMilitaryTime() @AutoMap() - thuTime?: string | null; + thuTime?: string; @IsOptional() - @IsString() + @IsMilitaryTime() @AutoMap() - friTime?: string | null; + friTime?: string; @IsOptional() - @IsString() + @IsMilitaryTime() @AutoMap() - satTime?: string | null; + satTime?: string; @IsOptional() - @IsString() + @IsMilitaryTime() @AutoMap() - sunTime?: string | null; + sunTime?: string; @IsNumber() @AutoMap() @@ -103,19 +111,33 @@ export class CreateAdRequest { @AutoMap() sunMargin: number; - @IsEnum(PointType) - @AutoMap() - originType: PointType; - - @IsEnum(PointType) - @AutoMap() - destinationType: PointType; - + @Type(() => Coordinates) @IsArray() @ArrayMinSize(2) @AutoMap(() => [Coordinates]) waypoints: Coordinates[]; + @AutoMap() + driverDuration?: number; + + @AutoMap() + driverDistance?: number; + + @AutoMap() + passengerDuration?: number; + + @AutoMap() + passengerDistance?: number; + + @AutoMap() + direction: string; + + @AutoMap() + fwdAzimuth: number; + + @AutoMap() + backAzimuth: number; + @IsNumber() @AutoMap() seatsDriver: number; @@ -129,13 +151,9 @@ export class CreateAdRequest { @AutoMap() seatsUsed?: number; - @IsString() + @IsBoolean() @AutoMap() - createdAt: string; - - @IsString() - @AutoMap() - updatedAt: string; + strict: boolean; timezone?: string; } diff --git a/src/modules/ad/domain/entities/ad.completer.ts b/src/modules/ad/domain/entities/ad.completer.ts new file mode 100644 index 0000000..2074621 --- /dev/null +++ b/src/modules/ad/domain/entities/ad.completer.ts @@ -0,0 +1,7 @@ +import { Ad } from './ad'; + +export class AdCompleter { + complete = async (ad: Ad): Promise => { + return ad; + }; +} diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index 1e4d1d6..0361c2f 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; -import { PointType } from '../../../geography/domain/types/point-type.enum'; import { Coordinates } from '../../../geography/domain/entities/coordinates'; +import { Frequency } from '../types/frequency.enum'; export class Ad { @AutoMap() @@ -13,7 +13,7 @@ export class Ad { passenger: boolean; @AutoMap() - frequency: number; + frequency: Frequency; @AutoMap() fromDate: Date; @@ -64,22 +64,16 @@ export class Ad { sunMargin: number; @AutoMap() - driverDuration: number; + driverDuration?: number; @AutoMap() - driverDistance: number; + driverDistance?: number; @AutoMap() - passengerDuration: number; + passengerDuration?: number; @AutoMap() - passengerDistance: number; - - @AutoMap() - originType: PointType; - - @AutoMap() - destinationType: PointType; + passengerDistance?: number; @AutoMap(() => [Coordinates]) waypoints: Coordinates[]; @@ -102,6 +96,9 @@ export class Ad { @AutoMap() seatsUsed: number; + @AutoMap() + strict: boolean; + @AutoMap() createdAt: 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..39836ac --- /dev/null +++ b/src/modules/ad/domain/entities/geography.ts @@ -0,0 +1,98 @@ +import { Coordinates } from '../../../geography/domain/entities/coordinates'; +import { Route } from '../../../geography/domain/entities/route'; +import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface'; +import { Role } from '../types/role.enum'; +import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface'; +import { Path } from '../../../geography/domain/types/path.type'; +import { Timezoner } from '../../../geography/domain/types/timezoner'; + +export class Geography { + private points: Coordinates[]; + timezones: string[]; + driverRoute: Route; + passengerRoute: Route; + timezoneFinder: IFindTimezone; + + constructor(points: Coordinates[], timezoner: Timezoner) { + this.points = points; + this.timezones = [timezoner.timezone]; + this.timezoneFinder = timezoner.finder; + this.setTimezones(); + } + + createRoutes = async ( + roles: Role[], + georouter: IGeorouter, + ): Promise => { + 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 + const commonPath: Path = { + key: RouteKey.COMMON, + points: this.points, + }; + paths.push(commonPath); + } else { + const driverPath: Path = { + key: RouteKey.DRIVER, + points: this.points, + }; + const passengerPath: Path = { + key: RouteKey.PASSENGER, + points: [this.points[0], this.points[this.points.length - 1]], + }; + paths.push(driverPath, passengerPath); + } + } else if (roles.includes(Role.DRIVER)) { + const driverPath: Path = { + key: RouteKey.DRIVER, + points: this.points, + }; + paths.push(driverPath); + } else if (roles.includes(Role.PASSENGER)) { + const passengerPath: Path = { + key: RouteKey.PASSENGER, + points: [this.points[0], this.points[this.points.length - 1]], + }; + paths.push(passengerPath); + } + const routes = await georouter.route(paths, { + withDistance: false, + withPoints: false, + withTime: false, + }); + 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 setTimezones = (): void => { + this.timezones = this.timezoneFinder.timezones( + this.points[0].lat, + this.points[0].lon, + ); + }; +} + +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 index bc0418a..e60bf76 100644 --- a/src/modules/ad/domain/entities/time-converter.ts +++ b/src/modules/ad/domain/entities/time-converter.ts @@ -1,14 +1,14 @@ import { DateTime, TimeZone } from 'timezonecomplete'; export class TimeConverter { - static toUtcDatetime = ( - date: string, - time: string, - timezone: string, - ): Date => { + static toUtcDatetime = (date: Date, time: string, timezone: string): Date => { try { + if (!date || !time || !timezone) throw new Error(); return new Date( - new DateTime(`${date}T${time}:00`, TimeZone.zone(timezone, false)) + new DateTime( + `${date.toISOString().split('T')[0]}T${time}:00`, + TimeZone.zone(timezone, false), + ) .convert(TimeZone.zone('UTC')) .toIsoString(), ); 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..89dcb0e --- /dev/null +++ b/src/modules/ad/domain/types/default-params.type.ts @@ -0,0 +1,5 @@ +export type IDefaultParams = { + 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 index ec435e8..0126874 100644 --- a/src/modules/ad/domain/types/frequency.enum.ts +++ b/src/modules/ad/domain/types/frequency.enum.ts @@ -1,4 +1,4 @@ export enum Frequency { - PUNCTUAL = 1, - RECURRENT = 2, + 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 index d85bf81..529ed34 100644 --- a/src/modules/ad/domain/usecases/create-ad.usecase.ts +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -20,6 +20,16 @@ export class CreateAdUseCase { CreateAdRequest, Ad, ); + adToCreate.driverDistance = command.geography.driverRoute?.distance; + adToCreate.driverDuration = command.geography.driverRoute?.duration; + adToCreate.passengerDistance = command.geography.passengerRoute?.distance; + adToCreate.passengerDuration = command.geography.passengerRoute?.duration; + adToCreate.fwdAzimuth = command.geography.driverRoute + ? command.geography.driverRoute.fwdAzimuth + : command.geography.passengerRoute.fwdAzimuth; + adToCreate.backAzimuth = command.geography.driverRoute + ? command.geography.driverRoute.backAzimuth + : command.geography.passengerRoute.backAzimuth; return adToCreate; // return await this.adRepository.createAd(adToCreate); } catch (error) { diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index c0b61f2..f491616 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -4,7 +4,6 @@ import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; -import { Coordinates } from '../../geography/domain/entities/coordinates'; import { TimeConverter } from '../domain/entities/time-converter'; @Injectable() @@ -16,43 +15,10 @@ export class AdProfile extends AutomapperProfile { override get profile() { return (mapper: any) => { createMap(mapper, Ad, AdPresenter); - createMap( - mapper, - CreateAdRequest, - CreateAdRequest, - forMember( - (dest) => dest.waypoints, - mapFrom((source) => - source.waypoints.map( - (waypoint) => - new Coordinates( - waypoint.lon ?? undefined, - waypoint.lat ?? undefined, - ), - ), - ), - ), - ); createMap( mapper, CreateAdRequest, Ad, - forMember( - (dest) => dest.fromDate, - mapFrom((source) => new Date(source.fromDate)), - ), - forMember( - (dest) => dest.toDate, - mapFrom((source) => new Date(source.toDate)), - ), - forMember( - (dest) => dest.createdAt, - mapFrom((source) => new Date(source.createdAt)), - ), - forMember( - (dest) => dest.updatedAt, - mapFrom((source) => new Date(source.updatedAt)), - ), forMember( (dest) => dest.monTime, mapFrom(({ monTime: time, fromDate: date, timezone }) => 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..4d32f12 --- /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 { IDefaultParams } 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: IDefaultParams = defaultParamsProvider.getParams(); + expect(params.GEOROUTER_URL).toBe('some_default_value'); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/ad.completer.spec.ts b/src/modules/ad/tests/unit/domain/ad.completer.spec.ts new file mode 100644 index 0000000..4d5db12 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/ad.completer.spec.ts @@ -0,0 +1,42 @@ +import { Frequency } from '../../../domain/types/frequency.enum'; +import { Ad } from '../../../domain/entities/ad'; +import { AdCompleter } from '../../../domain/entities/ad.completer'; +import { Coordinates } from '../../../../geography/domain/entities/coordinates'; + +const ad: Ad = new Ad(); +ad.driver = true; +ad.passenger = false; +ad.frequency = Frequency.RECURRENT; +ad.fromDate = new Date('2023-05-01'); +ad.toDate = new Date('2024-05-01'); +ad.monTime = new Date('2023-05-01T06:00:00.000Z'); +ad.tueTime = new Date('2023-05-01T06:00:00.000Z'); +ad.wedTime = new Date('2023-05-01T06:00:00.000Z'); +ad.thuTime = new Date('2023-05-01T06:00:00.000Z'); +ad.friTime = new Date('2023-05-01T06:00:00.000Z'); +ad.monMargin = 900; +ad.tueMargin = 900; +ad.wedMargin = 900; +ad.thuMargin = 900; +ad.friMargin = 900; +ad.satMargin = 900; +ad.sunMargin = 900; +ad.waypoints = [new Coordinates(6.18, 48.69), new Coordinates(6.44, 48.85)]; +ad.seatsDriver = 3; +ad.seatsPassenger = 1; +ad.strict = false; + +describe('AdCompleter', () => { + it('should be defined', () => { + expect(new AdCompleter()).toBeDefined(); + }); + + describe('complete', () => { + it('should complete an ad', async () => { + const ad: Ad = new Ad(); + const adCompleter: AdCompleter = new AdCompleter(); + const completedAd: Ad = await adCompleter.complete(ad); + expect(completedAd.fwdAzimuth).toBe(45); + }); + }); +}); 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 index c879974..d7eee5a 100644 --- a/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts +++ b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts @@ -1,5 +1,4 @@ import { CreateAdRequest } from '../../../domain/dtos/create-ad.request'; -import { PointType } from '../../../../geography/domain/types/point-type.enum'; import { CreateAdUseCase } from '../../../domain/usecases/create-ad.usecase'; import { Test, TestingModule } from '@nestjs/testing'; import { AutomapperModule } from '@automapper/nestjs'; @@ -8,16 +7,28 @@ 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 { IDefaultParams } from '../../../domain/types/default-params.type'; const mockAdRepository = {}; +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + GEOROUTER_TYPE: 'graphhopper', + GEOROUTER_URL: 'http://localhost', +}; + const createAdRequest: CreateAdRequest = { uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1', + userUuid: 'dfd993f6-7889-4876-9570-5e1d7b6e3f42', driver: true, passenger: false, - frequency: 2, - fromDate: '2023-04-26', - toDate: '2024-04-25', + frequency: Frequency.RECURRENT, + fromDate: new Date('2023-04-26'), + toDate: new Date('2024-04-25'), monTime: '07:00', tueTime: '07:00', wedTime: '07:00', @@ -32,12 +43,9 @@ const createAdRequest: CreateAdRequest = { friMargin: 900, satMargin: 900, sunMargin: 900, - originType: PointType.OTHER, - destinationType: PointType.OTHER, seatsDriver: 3, seatsPassenger: 1, - createdAt: '2023-04-01 12:45', - updatedAt: '2023-04-01 12:45', + strict: false, waypoints: [ { lon: 6, lat: 45 }, { lon: 6.5, lat: 45.5 }, @@ -70,7 +78,11 @@ describe('CreateAdUseCase', () => { describe('execute', () => { it('should create an ad', async () => { const ad = await createAdUseCase.execute( - new CreateAdCommand(createAdRequest), + new CreateAdCommand( + createAdRequest, + defaultParams, + mockGeorouterCreator, + ), ); expect(ad).toBeInstanceOf(Ad); }); diff --git a/src/modules/ad/tests/unit/domain/time-converter.spec.ts b/src/modules/ad/tests/unit/domain/time-converter.spec.ts index 5539cd9..00138e4 100644 --- a/src/modules/ad/tests/unit/domain/time-converter.spec.ts +++ b/src/modules/ad/tests/unit/domain/time-converter.spec.ts @@ -8,7 +8,7 @@ describe('TimeConverter', () => { it('should convert a Europe/Paris datetime to utc datetime', () => { expect( TimeConverter.toUtcDatetime( - '2023-05-01', + new Date('2023-05-01'), '07:00', 'Europe/Paris', ).getUTCHours(), @@ -20,16 +20,28 @@ describe('TimeConverter', () => { TimeConverter.toUtcDatetime(undefined, '07:00', 'Europe/Paris'), ).toBeUndefined(); expect( - TimeConverter.toUtcDatetime('2023-13-01', '07:00', 'Europe/Paris'), + TimeConverter.toUtcDatetime( + new Date('2023-13-01'), + '07:00', + 'Europe/Paris', + ), ).toBeUndefined(); expect( - TimeConverter.toUtcDatetime('2023-05-01', undefined, 'Europe/Paris'), + TimeConverter.toUtcDatetime( + new Date('2023-05-01'), + undefined, + 'Europe/Paris', + ), ).toBeUndefined(); expect( - TimeConverter.toUtcDatetime('2023-05-01', 'a', 'Europe/Paris'), + TimeConverter.toUtcDatetime(new Date('2023-05-01'), 'a', 'Europe/Paris'), ).toBeUndefined(); expect( - TimeConverter.toUtcDatetime('2023-13-01', '07:00', 'OlympusMons/Mars'), + TimeConverter.toUtcDatetime( + new Date('2023-13-01'), + '07:00', + 'OlympusMons/Mars', + ), ).toBeUndefined(); }); }); diff --git a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts index dee142c..635e966 100644 --- a/src/modules/database/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, }); @@ -191,7 +191,7 @@ export abstract class PrismaRepository implements IRepository { where: string[], ): Promise> { const query = `SELECT ${include.join(',')} FROM ${ - this._model + this.model } WHERE ${where.join(' AND ')}`; const data: T[] = await this._prisma.$queryRawUnsafe(query); return Promise.resolve({ @@ -202,7 +202,7 @@ export abstract class PrismaRepository implements IRepository { async createWithFields(fields: object): Promise { try { - const command = `INSERT INTO ${this._model} ("${Object.keys(fields).join( + const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join( '","', )}") VALUES (${Object.values(fields).join(',')})`; return await this._prisma.$executeRawUnsafe(command); @@ -223,7 +223,7 @@ export abstract class PrismaRepository implements IRepository { 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/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index 984c69d..eb3bad0 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -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/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..b573533 --- /dev/null +++ b/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts @@ -0,0 +1,324 @@ +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()) + .join('&point='), + ].join(''); + const route = await lastValueFrom( + this.httpService.get(url).pipe( + map((res) => (res.data ? this.createRoute(res) : undefined)), + catchError((error: AxiosError) => { + throw new GeographyException( + ExceptionCode.INTERNAL, + '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/matcher/domain/entities/ecosystem/route.ts b/src/modules/geography/domain/entities/route.ts similarity index 72% rename from src/modules/matcher/domain/entities/ecosystem/route.ts rename to src/modules/geography/domain/entities/route.ts index c42dba9..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 '../../../../geography/domain/interfaces/geodesic.interface'; -import { Point } from '../../../../geography/domain/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,7 +8,6 @@ export class Route { fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - waypoints: Waypoint[]; points: Point[]; spacetimePoints: SpacetimePoint[]; private geodesic: IGeodesic; @@ -20,17 +18,11 @@ export class Route { this.fwdAzimuth = undefined; this.backAzimuth = undefined; this.distanceAzimuth = undefined; - this.waypoints = []; this.points = []; this.spacetimePoints = []; this.geodesic = geodesic; } - setWaypoints = (waypoints: Waypoint[]): void => { - this.waypoints = waypoints; - this.setAzimuth(waypoints.map((waypoint) => waypoint.point)); - }; - setPoints = (points: Point[]): void => { this.points = points; this.setAzimuth(points); @@ -40,7 +32,7 @@ export class Route { this.spacetimePoints = spacetimePoints; }; - private setAzimuth = (points: Point[]): 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..97f130d --- /dev/null +++ b/src/modules/geography/domain/entities/spacetime-point.ts @@ -0,0 +1,13 @@ +import { Coordinates } from './coordinates'; + +export class SpacetimePoint { + coordinates: Coordinates; + duration: number; + distance: number; + + constructor(coordinates: Coordinates, duration: number, distance: number) { + this.coordinates = coordinates; + this.duration = duration; + this.distance = distance; + } +} 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/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/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..9d07939 --- /dev/null +++ b/src/modules/geography/exceptions/geography.exception.ts @@ -0,0 +1,13 @@ +export class GeographyException implements Error { + name: string; + message: string; + + constructor(private _code: number, private _message: string) { + this.name = 'GeographyException'; + this.message = _message; + } + + get code(): number { + return this._code; + } +} 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/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/matcher/adapters/primaries/matcher.controller.ts b/src/modules/matcher/adapters/primaries/matcher.controller.ts index 0bbabbf..bc926b2 100644 --- a/src/modules/matcher/adapters/primaries/matcher.controller.ts +++ b/src/modules/matcher/adapters/primaries/matcher.controller.ts @@ -25,7 +25,7 @@ export class MatcherController { constructor( private readonly queryBus: QueryBus, private readonly defaultParamsProvider: DefaultParamsProvider, - @InjectMapper() private readonly _mapper: Mapper, + @InjectMapper() private readonly mapper: Mapper, private readonly georouterCreator: GeorouterCreator, private readonly timezoneFinder: GeoTimezoneFinder, private readonly timeConverter: TimeConverter, @@ -45,7 +45,7 @@ export class MatcherController { ); 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 f18aaff..898e6ee 100644 --- a/src/modules/matcher/adapters/primaries/matcher.proto +++ b/src/modules/matcher/adapters/primaries/matcher.proto @@ -7,29 +7,28 @@ service MatcherService { } message MatchRequest { - Mode mode = 1; - string uuid = 2; - repeated Coordinates waypoints = 3; - string departure = 4; - string fromDate = 5; - Schedule schedule = 6; - bool driver = 7; - bool passenger = 8; - string toDate = 9; - int32 marginDuration = 10; - MarginDurations marginDurations = 11; - int32 seatsPassenger = 12; - int32 seatsDriver = 13; - bool strict = 14; - Algorithm algorithm = 15; - int32 remoteness = 16; - bool useProportion = 17; - int32 proportion = 18; - bool useAzimuth = 19; - int32 azimuthMargin = 20; - float maxDetourDistanceRatio = 21; - float maxDetourDurationRatio = 22; - repeated int32 exclusions = 23; + 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 Coordinates { @@ -69,9 +68,3 @@ message Matches { repeated Match data = 1; int32 total = 2; } - -enum Mode { - MATCH = 0; - PUBLISH = 1; - PUBLISH_AND_MATCH = 2; -} diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index 5bb3158..d331919 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -14,21 +14,21 @@ export class DefaultParamsProvider { 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/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 935eddb..472a333 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -7,7 +7,7 @@ import { catchError, lastValueFrom, map } from 'rxjs'; import { AxiosError, AxiosResponse } from 'axios'; 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, @@ -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; diff --git a/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts index 158ddfd..0e5dd92 100644 --- a/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts +++ b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts @@ -27,33 +27,33 @@ export class AlgorithmSettings { ) { 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 9afd432..abf9be7 100644 --- a/src/modules/matcher/domain/entities/ecosystem/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -5,7 +5,7 @@ import { import { IRequestGeography } from '../../interfaces/geography-request.interface'; import { PointType } from '../../../../geography/domain/types/point-type.enum'; import { Point } from '../../../../geography/domain/types/point.type'; -import { Route } from './route'; +import { MatcherRoute } from './matcher-route'; import { Role } from '../../types/role.enum'; import { IGeorouter } from '../../interfaces/georouter.interface'; import { Waypoint } from './waypoint'; @@ -23,8 +23,8 @@ export class Geography { originType: PointType; destinationType: PointType; timezones: string[]; - driverRoute: Route; - passengerRoute: Route; + driverRoute: MatcherRoute; + passengerRoute: MatcherRoute; timezoneFinder: IFindTimezone; constructor( 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/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/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index c834224..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: Match[] = 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 39e1dfc..fe84110 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -19,6 +19,7 @@ import { AlgorithmFactoryCreator } from './domain/entities/engine/factory/algori 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: [ @@ -62,6 +63,7 @@ import { GeographyModule } from '../geography/geography.module'; GeorouterCreator, MatcherGeodesic, TimezoneFinder, + TimeConverter, Matcher, AlgorithmFactoryCreator, GeoTimezoneFinder, 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 82925c7..3480b2b 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -5,7 +5,7 @@ import { } 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 { MatcherRoute } from '../../../../domain/entities/ecosystem/matcher-route'; import { IGeodesic } from '../../../../../geography/domain/interfaces/geodesic.interface'; import { PointType } from '../../../../../geography/domain/types/point-type.enum'; @@ -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,7 +59,7 @@ const mockGeorouter = { return [ { key: RouteKey.PASSENGER, - route: new Route(mockGeodesic), + route: new MatcherRoute(mockGeodesic), }, ]; }), 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 84% 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 70c9460..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,7 +56,7 @@ describe('Route entity', () => { expect(route.distanceAzimuth).toBe(60000); }); it('should set spacetimePoints for a route', () => { - const route = new Route(mockGeodesic); + 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]); 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 e9c076b..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 f4b62c2..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 @@ -26,17 +26,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 070b8ee..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 a6272d6..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 825a186..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 961db3f..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 d20cd56..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 3dc02f4..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 09fef45..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 5f6c135..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 c4ce2fd..5b73254 100644 --- a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts +++ b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts @@ -36,17 +36,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 34948a6..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 a1ac083..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 a681135..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 @@ -24,17 +24,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 10d97aa..8ed9acf 100644 --- a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -49,17 +49,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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 bc14991..b11a0e5 100644 --- a/src/modules/matcher/tests/unit/queries/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -13,17 +13,17 @@ const defaultParams: IDefaultParams = { 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', }, }; 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, +} From e950efe2211083c48280e9fef7f964bbbbbc97ca Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 12 May 2023 16:23:42 +0200 Subject: [PATCH 14/19] create ad, WIP --- .../20230512130750_init/migration.sql | 68 ++++++++++ prisma/migrations/migration_lock.toml | 3 + src/modules/ad/ad.module.ts | 22 +++- .../primaries/ad-messager.controller.ts | 21 +-- .../ad/adapters/secondaries/ad.repository.ts | 75 ++++++++--- .../secondaries/default-params.provider.ts | 7 +- src/modules/ad/commands/create-ad.command.ts | 47 +------ .../ad/domain/dtos/create-ad.request.ts | 27 +--- .../domain/dtos/has-truthy-with.validator.ts | 32 +++++ .../ad/domain/entities/ad.completer.ts | 7 - src/modules/ad/domain/entities/ad.ts | 8 +- src/modules/ad/domain/entities/geography.ts | 8 +- .../interfaces/params-provider.interface.ts | 5 + .../ad/domain/types/default-params.type.ts | 2 +- .../ad/domain/usecases/create-ad.usecase.ts | 124 +++++++++++++++--- src/modules/ad/mappers/ad.profile.ts | 51 +------ .../default-params.provider.spec.ts | 4 +- .../ad/tests/unit/domain/ad.completer.spec.ts | 42 ------ .../unit/domain/create-ad.usecase.spec.ts | 75 ++++++----- .../secondaries/prisma-repository.abstract.ts | 1 + .../secondaries/graphhopper-georouter.ts | 10 +- .../secondaries/postgres-direction-encoder.ts | 11 ++ .../interfaces/direction-encoder.interface.ts | 5 + .../exceptions/geography.exception.ts | 10 +- 24 files changed, 382 insertions(+), 283 deletions(-) create mode 100644 prisma/migrations/20230512130750_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/modules/ad/domain/dtos/has-truthy-with.validator.ts delete mode 100644 src/modules/ad/domain/entities/ad.completer.ts create mode 100644 src/modules/ad/domain/interfaces/params-provider.interface.ts delete mode 100644 src/modules/ad/tests/unit/domain/ad.completer.spec.ts create mode 100644 src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts create mode 100644 src/modules/geography/domain/interfaces/direction-encoder.interface.ts 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/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index bc23450..3a936b0 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -8,12 +8,12 @@ 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 { TimezoneFinder } from './adapters/secondaries/timezone-finder'; 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: [ @@ -46,14 +46,26 @@ import { HttpModule } from '@nestjs/axios'; ], controllers: [AdMessagerController], providers: [ + { + provide: 'ParamsProvider', + useClass: DefaultParamsProvider, + }, + { + provide: 'GeorouterCreator', + useClass: GeorouterCreator, + }, + { + provide: 'TimezoneFinder', + useClass: GeoTimezoneFinder, + }, + { + provide: 'DirectionEncoder', + useClass: PostgresDirectionEncoder, + }, AdProfile, Messager, AdRepository, - TimezoneFinder, - GeoTimezoneFinder, CreateAdUseCase, - DefaultParamsProvider, - GeorouterCreator, ], exports: [], }) diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts index c122475..9774222 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -1,27 +1,18 @@ import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; import { Controller } from '@nestjs/common'; import { Ad } from '../../domain/entities/ad'; -import { InjectMapper } from '@automapper/nestjs'; -import { Mapper } from '@automapper/core'; 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 { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder'; import { plainToInstance } from 'class-transformer'; -import { DefaultParamsProvider } from '../secondaries/default-params.provider'; -import { GeorouterCreator } from '../../../geography/adapters/secondaries/georouter-creator'; @Controller() export class AdMessagerController { constructor( private readonly messager: Messager, private readonly commandBus: CommandBus, - @InjectMapper() private readonly mapper: Mapper, - private readonly defaultParamsProvider: DefaultParamsProvider, - private readonly georouterCreator: GeorouterCreator, - private readonly timezoneFinder: GeoTimezoneFinder, ) {} @RabbitSubscribe({ @@ -29,6 +20,7 @@ export class AdMessagerController { }) async adCreatedHandler(message: string): Promise { try { + // parse message to request instance const createAdRequest: CreateAdRequest = plainToInstance( CreateAdRequest, JSON.parse(message), @@ -43,17 +35,8 @@ export class AdMessagerController { throw e; } } - createAdRequest.timezone = this.timezoneFinder.timezones( - createAdRequest.waypoints[0].lon, - createAdRequest.waypoints[0].lat, - )[0]; const ad: Ad = await this.commandBus.execute( - new CreateAdCommand( - createAdRequest, - this.defaultParamsProvider.getParams(), - this.georouterCreator, - this.timezoneFinder, - ), + new CreateAdCommand(createAdRequest), ); console.log(ad); } catch (e) { diff --git a/src/modules/ad/adapters/secondaries/ad.repository.ts b/src/modules/ad/adapters/secondaries/ad.repository.ts index 11aa335..0c83052 100644 --- a/src/modules/ad/adapters/secondaries/ad.repository.ts +++ b/src/modules/ad/adapters/secondaries/ad.repository.ts @@ -2,7 +2,6 @@ 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'; -import { Frequency } from '../../domain/types/frequency.enum'; @Injectable() export class AdRepository extends MatcherRepository { @@ -25,27 +24,62 @@ export class AdRepository extends MatcherRepository { 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}'`, - toDate: `'${ad.toDate}'`, - monTime: `'${ad.monTime}'`, - tueTime: `'${ad.tueTime}'`, - wedTime: `'${ad.wedTime}'`, - thuTime: `'${ad.thuTime}'`, - friTime: `'${ad.friTime}'`, - satTime: `'${ad.satTime}'`, - sunTime: `'${ad.sunTime}'`, + frequency: `'${ad.frequency}'`, + fromDate: `'${ad.fromDate.getFullYear()}-${ad.fromDate.getMonth()}-${ad.fromDate.getDate()}'`, + toDate: `'${ad.toDate.getFullYear()}-${ad.toDate.getMonth()}-${ad.toDate.getDate()}'`, + monTime: ad.monTime + ? `'${ad.monTime.getFullYear()}-${ad.monTime.getMonth()}-${ad.monTime.getDate()}T${ad.monTime.getHours()}:${ad.monTime.getMinutes()}Z'` + : 'NULL', + tueTime: ad.tueTime + ? `'${ad.tueTime.getFullYear()}-${ad.tueTime.getMonth()}-${ad.tueTime.getDate()}T${ad.tueTime.getHours()}:${ad.tueTime.getMinutes()}Z'` + : 'NULL', + wedTime: ad.wedTime + ? `'${ad.wedTime.getFullYear()}-${ad.wedTime.getMonth()}-${ad.wedTime.getDate()}T${ad.wedTime.getHours()}:${ad.wedTime.getMinutes()}Z'` + : 'NULL', + thuTime: ad.thuTime + ? `'${ad.thuTime.getFullYear()}-${ad.thuTime.getMonth()}-${ad.thuTime.getDate()}T${ad.thuTime.getHours()}:${ad.thuTime.getMinutes()}Z'` + : 'NULL', + friTime: ad.friTime + ? `'${ad.friTime.getFullYear()}-${ad.friTime.getMonth()}-${ad.friTime.getDate()}T${ad.friTime.getHours()}:${ad.friTime.getMinutes()}Z'` + : 'NULL', + satTime: ad.satTime + ? `'${ad.satTime.getFullYear()}-${ad.satTime.getMonth()}-${ad.satTime.getDate()}T${ad.satTime.getHours()}:${ad.satTime.getMinutes()}Z'` + : 'NULL', + sunTime: ad.sunTime + ? `'${ad.sunTime.getFullYear()}-${ad.sunTime.getMonth()}-${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: Frequency; + frequency: string; fromDate: string; toDate: string; monTime: string; @@ -62,19 +96,18 @@ type AdFields = { friMargin: number; satMargin: number; sunMargin: number; - driverDuration: number; - driverDistance: number; - passengerDuration: number; - passengerDistance: number; - originType: number; - destinationType: 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; + 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 index 62e45aa..f0a41fb 100644 --- a/src/modules/ad/adapters/secondaries/default-params.provider.ts +++ b/src/modules/ad/adapters/secondaries/default-params.provider.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { IDefaultParams } from '../../domain/types/default-params.type'; +import { DefaultParams } from '../../domain/types/default-params.type'; +import { IProvideParams } from '../../domain/interfaces/params-provider.interface'; @Injectable() -export class DefaultParamsProvider { +export class DefaultParamsProvider implements IProvideParams { constructor(private readonly configService: ConfigService) {} - getParams = (): IDefaultParams => { + getParams = (): DefaultParams => { return { DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'), GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'), diff --git a/src/modules/ad/commands/create-ad.command.ts b/src/modules/ad/commands/create-ad.command.ts index d1e1d0a..b4f1e8d 100644 --- a/src/modules/ad/commands/create-ad.command.ts +++ b/src/modules/ad/commands/create-ad.command.ts @@ -1,54 +1,9 @@ -import { IGeorouter } from '../../geography/domain/interfaces/georouter.interface'; -import { ICreateGeorouter } from '../../geography/domain/interfaces/georouter-creator.interface'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; -import { Geography } from '../domain/entities/geography'; -import { IDefaultParams } from '../domain/types/default-params.type'; -import { Role } from '../domain/types/role.enum'; -import { IFindTimezone } from '../../geography/domain/interfaces/timezone-finder.interface'; export class CreateAdCommand { readonly createAdRequest: CreateAdRequest; - private readonly defaultParams: IDefaultParams; - private readonly georouter: IGeorouter; - private readonly timezoneFinder: IFindTimezone; - roles: Role[]; - geography: Geography; - timezone: string; - constructor( - request: CreateAdRequest, - defaultParams: IDefaultParams, - georouterCreator: ICreateGeorouter, - timezoneFinder: IFindTimezone, - ) { + constructor(request: CreateAdRequest) { this.createAdRequest = request; - this.defaultParams = defaultParams; - this.georouter = georouterCreator.create( - defaultParams.GEOROUTER_TYPE, - defaultParams.GEOROUTER_URL, - ); - this.timezoneFinder = timezoneFinder; - this.setRoles(); - this.setGeography(); } - - private setRoles = (): void => { - this.roles = []; - if (this.createAdRequest.driver) this.roles.push(Role.DRIVER); - if (this.createAdRequest.passenger) this.roles.push(Role.PASSENGER); - }; - - private setGeography = async (): Promise => { - this.geography = new Geography(this.createAdRequest.waypoints, { - timezone: this.defaultParams.DEFAULT_TIMEZONE, - finder: this.timezoneFinder, - }); - if (this.geography.timezones.length > 0) - this.createAdRequest.timezone = this.geography.timezones[0]; - try { - await this.geography.createRoutes(this.roles, this.georouter); - } catch (e) { - console.log(e); - } - }; } diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index 7dce0b1..be87f29 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -14,6 +14,7 @@ import { import { Frequency } from '../types/frequency.enum'; import { Coordinates } from '../../../geography/domain/entities/coordinates'; import { Type } from 'class-transformer'; +import { HasTruthyWith } from './has-truthy-with.validator'; export class CreateAdRequest { @IsString() @@ -26,6 +27,9 @@ export class CreateAdRequest { @AutoMap() userUuid: string; + @HasTruthyWith('passenger', { + message: 'A role (driver or passenger) must be set to true', + }) @IsBoolean() @AutoMap() driver: boolean; @@ -117,27 +121,6 @@ export class CreateAdRequest { @AutoMap(() => [Coordinates]) waypoints: Coordinates[]; - @AutoMap() - driverDuration?: number; - - @AutoMap() - driverDistance?: number; - - @AutoMap() - passengerDuration?: number; - - @AutoMap() - passengerDistance?: number; - - @AutoMap() - direction: string; - - @AutoMap() - fwdAzimuth: number; - - @AutoMap() - backAzimuth: number; - @IsNumber() @AutoMap() seatsDriver: number; @@ -154,6 +137,4 @@ export class CreateAdRequest { @IsBoolean() @AutoMap() strict: boolean; - - timezone?: string; } 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.completer.ts b/src/modules/ad/domain/entities/ad.completer.ts deleted file mode 100644 index 2074621..0000000 --- a/src/modules/ad/domain/entities/ad.completer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Ad } from './ad'; - -export class AdCompleter { - complete = async (ad: Ad): Promise => { - return ad; - }; -} diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index 0361c2f..a3a7ffb 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -1,11 +1,13 @@ import { AutoMap } from '@automapper/classes'; -import { Coordinates } from '../../../geography/domain/entities/coordinates'; import { Frequency } from '../types/frequency.enum'; export class Ad { @AutoMap() uuid: string; + @AutoMap() + userUuid: string; + @AutoMap() driver: boolean; @@ -75,8 +77,8 @@ export class Ad { @AutoMap() passengerDistance?: number; - @AutoMap(() => [Coordinates]) - waypoints: Coordinates[]; + @AutoMap() + waypoints: string; @AutoMap() direction: string; diff --git a/src/modules/ad/domain/entities/geography.ts b/src/modules/ad/domain/entities/geography.ts index 39836ac..fb0d06f 100644 --- a/src/modules/ad/domain/entities/geography.ts +++ b/src/modules/ad/domain/entities/geography.ts @@ -5,6 +5,7 @@ import { Role } from '../types/role.enum'; import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface'; import { Path } from '../../../geography/domain/types/path.type'; import { Timezoner } from '../../../geography/domain/types/timezoner'; +import { GeorouterSettings } from '../../../geography/domain/types/georouter-settings.type'; export class Geography { private points: Coordinates[]; @@ -23,6 +24,7 @@ export class Geography { createRoutes = async ( roles: Role[], georouter: IGeorouter, + settings: GeorouterSettings, ): Promise => { const paths: Path[] = []; if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { @@ -57,11 +59,7 @@ export class Geography { }; paths.push(passengerPath); } - const routes = await georouter.route(paths, { - withDistance: false, - withPoints: false, - withTime: false, - }); + const routes = await georouter.route(paths, settings); if (routes.some((route) => route.key == RouteKey.COMMON)) { this.driverRoute = routes.find( (route) => route.key == RouteKey.COMMON, 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 index 89dcb0e..bea841b 100644 --- a/src/modules/ad/domain/types/default-params.type.ts +++ b/src/modules/ad/domain/types/default-params.type.ts @@ -1,4 +1,4 @@ -export type IDefaultParams = { +export type DefaultParams = { DEFAULT_TIMEZONE: string; GEOROUTER_TYPE: string; GEOROUTER_URL: string; diff --git a/src/modules/ad/domain/usecases/create-ad.usecase.ts b/src/modules/ad/domain/usecases/create-ad.usecase.ts index 529ed34..8621933 100644 --- a/src/modules/ad/domain/usecases/create-ad.usecase.ts +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -5,35 +5,125 @@ 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'; @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.timezone = this.defaultParams.DEFAULT_TIMEZONE; + this.georouter = georouterCreator.create( + this.defaultParams.GEOROUTER_TYPE, + this.defaultParams.GEOROUTER_URL, + ); + } async execute(command: CreateAdCommand): Promise { try { - const adToCreate: Ad = this.mapper.map( - command.createAdRequest, - CreateAdRequest, - Ad, + this.ad = this.mapper.map(command.createAdRequest, CreateAdRequest, Ad); + this.setRoles(command.createAdRequest); + this.setGeography(command.createAdRequest); + await this.geography.createRoutes(this.roles, this.georouter, { + withDistance: false, + withPoints: true, + withTime: false, + }); + 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, ); - adToCreate.driverDistance = command.geography.driverRoute?.distance; - adToCreate.driverDuration = command.geography.driverRoute?.duration; - adToCreate.passengerDistance = command.geography.passengerRoute?.distance; - adToCreate.passengerDuration = command.geography.passengerRoute?.duration; - adToCreate.fwdAzimuth = command.geography.driverRoute - ? command.geography.driverRoute.fwdAzimuth - : command.geography.passengerRoute.fwdAzimuth; - adToCreate.backAzimuth = command.geography.driverRoute - ? command.geography.driverRoute.backAzimuth - : command.geography.passengerRoute.backAzimuth; - return adToCreate; - // return await this.adRepository.createAd(adToCreate); + this.ad.direction = this.geography.driverRoute + ? this.directionEncoder.encode(this.geography.driverRoute.points) + : undefined; + 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, + ); + return await this.adRepository.createAd(this.ad); } catch (error) { throw error; } } + + 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 = (createAdRequest: CreateAdRequest): void => { + this.geography = new Geography(createAdRequest.waypoints, { + timezone: this.defaultParams.DEFAULT_TIMEZONE, + finder: this.timezoneFinder, + }); + if (this.geography.timezones.length > 0) + this.timezone = this.geography.timezones[0]; + }; } diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index f491616..7b7de92 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -1,10 +1,9 @@ -import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { createMap, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; -import { TimeConverter } from '../domain/entities/time-converter'; @Injectable() export class AdProfile extends AutomapperProfile { @@ -15,53 +14,7 @@ export class AdProfile extends AutomapperProfile { override get profile() { return (mapper: any) => { createMap(mapper, Ad, AdPresenter); - createMap( - mapper, - CreateAdRequest, - Ad, - forMember( - (dest) => dest.monTime, - mapFrom(({ monTime: time, fromDate: date, timezone }) => - TimeConverter.toUtcDatetime(date, time, timezone), - ), - ), - forMember( - (dest) => dest.tueTime, - mapFrom(({ tueTime: time, fromDate: date, timezone }) => - TimeConverter.toUtcDatetime(date, time, timezone), - ), - ), - forMember( - (dest) => dest.wedTime, - mapFrom(({ wedTime: time, fromDate: date, timezone }) => - TimeConverter.toUtcDatetime(date, time, timezone), - ), - ), - forMember( - (dest) => dest.thuTime, - mapFrom(({ thuTime: time, fromDate: date, timezone }) => - TimeConverter.toUtcDatetime(date, time, timezone), - ), - ), - forMember( - (dest) => dest.friTime, - mapFrom(({ friTime: time, fromDate: date, timezone }) => - TimeConverter.toUtcDatetime(date, time, timezone), - ), - ), - forMember( - (dest) => dest.satTime, - mapFrom(({ satTime: time, fromDate: date, timezone }) => - TimeConverter.toUtcDatetime(date, time, timezone), - ), - ), - forMember( - (dest) => dest.sunTime, - mapFrom(({ sunTime: time, fromDate: date, timezone }) => - TimeConverter.toUtcDatetime(date, time, timezone), - ), - ), - ); + createMap(mapper, CreateAdRequest, Ad); }; } } 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 index 4d32f12..5b69430 100644 --- 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 @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider'; -import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { DefaultParams } from '../../../../domain/types/default-params.type'; const mockConfigService = { get: jest.fn().mockImplementation(() => 'some_default_value'), @@ -32,7 +32,7 @@ describe('DefaultParamsProvider', () => { }); it('should provide default params', async () => { - const params: IDefaultParams = defaultParamsProvider.getParams(); + const params: DefaultParams = defaultParamsProvider.getParams(); expect(params.GEOROUTER_URL).toBe('some_default_value'); }); }); diff --git a/src/modules/ad/tests/unit/domain/ad.completer.spec.ts b/src/modules/ad/tests/unit/domain/ad.completer.spec.ts deleted file mode 100644 index 4d5db12..0000000 --- a/src/modules/ad/tests/unit/domain/ad.completer.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Frequency } from '../../../domain/types/frequency.enum'; -import { Ad } from '../../../domain/entities/ad'; -import { AdCompleter } from '../../../domain/entities/ad.completer'; -import { Coordinates } from '../../../../geography/domain/entities/coordinates'; - -const ad: Ad = new Ad(); -ad.driver = true; -ad.passenger = false; -ad.frequency = Frequency.RECURRENT; -ad.fromDate = new Date('2023-05-01'); -ad.toDate = new Date('2024-05-01'); -ad.monTime = new Date('2023-05-01T06:00:00.000Z'); -ad.tueTime = new Date('2023-05-01T06:00:00.000Z'); -ad.wedTime = new Date('2023-05-01T06:00:00.000Z'); -ad.thuTime = new Date('2023-05-01T06:00:00.000Z'); -ad.friTime = new Date('2023-05-01T06:00:00.000Z'); -ad.monMargin = 900; -ad.tueMargin = 900; -ad.wedMargin = 900; -ad.thuMargin = 900; -ad.friMargin = 900; -ad.satMargin = 900; -ad.sunMargin = 900; -ad.waypoints = [new Coordinates(6.18, 48.69), new Coordinates(6.44, 48.85)]; -ad.seatsDriver = 3; -ad.seatsPassenger = 1; -ad.strict = false; - -describe('AdCompleter', () => { - it('should be defined', () => { - expect(new AdCompleter()).toBeDefined(); - }); - - describe('complete', () => { - it('should complete an ad', async () => { - const ad: Ad = new Ad(); - const adCompleter: AdCompleter = new AdCompleter(); - const completedAd: Ad = await adCompleter.complete(ad); - expect(completedAd.fwdAzimuth).toBe(45); - }); - }); -}); 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 index d7eee5a..9079642 100644 --- a/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts +++ b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts @@ -8,18 +8,30 @@ 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 { IDefaultParams } from '../../../domain/types/default-params.type'; +import { RouteKey } from '../../../domain/entities/geography'; const mockAdRepository = {}; - const mockGeorouterCreator = { - create: jest.fn().mockImplementation(), + create: jest.fn().mockImplementation(() => ({ + route: jest.fn().mockImplementation(() => [ + { + key: RouteKey.COMMON, + points: [], + }, + ]), + })), }; - -const defaultParams: IDefaultParams = { - GEOROUTER_TYPE: 'graphhopper', - GEOROUTER_URL: 'http://localhost', +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 = {}; const createAdRequest: CreateAdRequest = { uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1', @@ -63,6 +75,22 @@ describe('CreateAdUseCase', () => { provide: AdRepository, useValue: mockAdRepository, }, + { + provide: 'GeorouterCreator', + useValue: mockGeorouterCreator, + }, + { + provide: 'ParamsProvider', + useValue: mockParamsProvider, + }, + { + provide: 'TimezoneFinder', + useValue: mockTimezoneFinder, + }, + { + provide: 'DirectionEncoder', + useValue: mockDirectionEncoder, + }, AdProfile, CreateAdUseCase, ], @@ -75,29 +103,12 @@ describe('CreateAdUseCase', () => { expect(createAdUseCase).toBeDefined(); }); - describe('execute', () => { - it('should create an ad', async () => { - const ad = await createAdUseCase.execute( - new CreateAdCommand( - createAdRequest, - defaultParams, - mockGeorouterCreator, - ), - ); - expect(ad).toBeInstanceOf(Ad); - }); - - // it('should throw an exception when error occurs', async () => { - // await expect( - // createAdUseCase.execute( - // new MatchQuery( - // matchRequest, - // defaultParams, - // mockGeorouterCreator, - // mockTimezoneFinder, - // ), - // ), - // ).rejects.toBeInstanceOf(MatcherException); - // }); - }); + // describe('execute', () => { + // it('should create an ad', async () => { + // const ad = await createAdUseCase.execute( + // new CreateAdCommand(createAdRequest), + // ); + // expect(ad).toBeInstanceOf(Ad); + // }); + // }); }); diff --git a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts index 635e966..be96d47 100644 --- a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts @@ -205,6 +205,7 @@ export abstract class PrismaRepository implements IRepository { const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join( '","', )}") VALUES (${Object.values(fields).join(',')})`; + console.log(command); return await this._prisma.$executeRawUnsafe(command); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) { diff --git a/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts b/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts index b573533..fd83d2b 100644 --- a/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/geography/adapters/secondaries/graphhopper-georouter.ts @@ -75,15 +75,21 @@ export class GraphhopperGeorouter implements IGeorouter { this.getUrl(), '&point=', path.points - .map((point) => [point.lat, point.lon].join()) + .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.INTERNAL, + ExceptionCode.UNAVAILABLE, 'Georouter unavailable : ' + error.message, ); }), 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..8e249c0 --- /dev/null +++ b/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts @@ -0,0 +1,11 @@ +import { Coordinates } from '../../domain/entities/coordinates'; +import { IEncodeDirection } from '../../domain/interfaces/direction-encoder.interface'; + +export class PostgresDirectionEncoder implements IEncodeDirection { + encode = (coordinates: Coordinates[]): string => + [ + "'LINESTRING(", + coordinates.map((point) => [point.lon, point.lat].join(' ')).join(), + ")'", + ].join(''); +} 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..52a5ce1 --- /dev/null +++ b/src/modules/geography/domain/interfaces/direction-encoder.interface.ts @@ -0,0 +1,5 @@ +import { Coordinates } from '../entities/coordinates'; + +export interface IEncodeDirection { + encode(coordinates: Coordinates[]): string; +} diff --git a/src/modules/geography/exceptions/geography.exception.ts b/src/modules/geography/exceptions/geography.exception.ts index 9d07939..ebc1813 100644 --- a/src/modules/geography/exceptions/geography.exception.ts +++ b/src/modules/geography/exceptions/geography.exception.ts @@ -1,13 +1,11 @@ export class GeographyException implements Error { name: string; + code: number; message: string; - constructor(private _code: number, private _message: string) { + constructor(code: number, message: string) { this.name = 'GeographyException'; - this.message = _message; - } - - get code(): number { - return this._code; + this.code = code; + this.message = message; } } From 17acaa449cd677c3962b2ddcc1447135c436472e Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 22 May 2023 11:25:09 +0200 Subject: [PATCH 15/19] tests and refactor for ad geography --- .../ad/domain/dtos/create-ad.request.ts | 8 +- src/modules/ad/domain/entities/geography.ts | 94 ++++++------ .../ad/domain/usecases/create-ad.usecase.ts | 25 ++-- .../ad/tests/unit/domain/geography.spec.ts | 138 ++++++++++++++++++ .../tests/unit/domain/time-converter.spec.ts | 10 +- .../secondaries/prisma-repository.abstract.ts | 1 - .../secondaries/postgres-direction-encoder.ts | 4 +- .../{coordinates.ts => coordinate.ts} | 2 +- .../domain/entities/spacetime-point.ts | 8 +- .../interfaces/direction-encoder.interface.ts | 4 +- .../geography/domain/types/point.type.ts | 4 +- .../geography/tests/unit/coordinate.spec.ts | 8 + .../unit/postgres-direction-encoder.spec.ts | 30 ++++ .../entities/ecosystem/spacetime-point.ts | 8 +- 14 files changed, 264 insertions(+), 80 deletions(-) create mode 100644 src/modules/ad/tests/unit/domain/geography.spec.ts rename src/modules/geography/domain/entities/{coordinates.ts => coordinate.ts} (92%) create mode 100644 src/modules/geography/tests/unit/coordinate.spec.ts create mode 100644 src/modules/geography/tests/unit/postgres-direction-encoder.spec.ts diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts index be87f29..58a1bb0 100644 --- a/src/modules/ad/domain/dtos/create-ad.request.ts +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -12,7 +12,7 @@ import { IsString, } from 'class-validator'; import { Frequency } from '../types/frequency.enum'; -import { Coordinates } from '../../../geography/domain/entities/coordinates'; +import { Coordinate } from '../../../geography/domain/entities/coordinate'; import { Type } from 'class-transformer'; import { HasTruthyWith } from './has-truthy-with.validator'; @@ -115,11 +115,11 @@ export class CreateAdRequest { @AutoMap() sunMargin: number; - @Type(() => Coordinates) + @Type(() => Coordinate) @IsArray() @ArrayMinSize(2) - @AutoMap(() => [Coordinates]) - waypoints: Coordinates[]; + @AutoMap(() => [Coordinate]) + waypoints: Coordinate[]; @IsNumber() @AutoMap() diff --git a/src/modules/ad/domain/entities/geography.ts b/src/modules/ad/domain/entities/geography.ts index fb0d06f..b720399 100644 --- a/src/modules/ad/domain/entities/geography.ts +++ b/src/modules/ad/domain/entities/geography.ts @@ -1,24 +1,17 @@ -import { Coordinates } from '../../../geography/domain/entities/coordinates'; +import { Coordinate } from '../../../geography/domain/entities/coordinate'; import { Route } from '../../../geography/domain/entities/route'; -import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface'; import { Role } from '../types/role.enum'; import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface'; import { Path } from '../../../geography/domain/types/path.type'; -import { Timezoner } from '../../../geography/domain/types/timezoner'; import { GeorouterSettings } from '../../../geography/domain/types/georouter-settings.type'; export class Geography { - private points: Coordinates[]; - timezones: string[]; + private coordinates: Coordinate[]; driverRoute: Route; passengerRoute: Route; - timezoneFinder: IFindTimezone; - constructor(points: Coordinates[], timezoner: Timezoner) { - this.points = points; - this.timezones = [timezoner.timezone]; - this.timezoneFinder = timezoner.finder; - this.setTimezones(); + constructor(coordinates: Coordinate[]) { + this.coordinates = coordinates; } createRoutes = async ( @@ -26,39 +19,7 @@ export class Geography { georouter: IGeorouter, settings: GeorouterSettings, ): Promise => { - 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 - const commonPath: Path = { - key: RouteKey.COMMON, - points: this.points, - }; - paths.push(commonPath); - } else { - const driverPath: Path = { - key: RouteKey.DRIVER, - points: this.points, - }; - const passengerPath: Path = { - key: RouteKey.PASSENGER, - points: [this.points[0], this.points[this.points.length - 1]], - }; - paths.push(driverPath, passengerPath); - } - } else if (roles.includes(Role.DRIVER)) { - const driverPath: Path = { - key: RouteKey.DRIVER, - points: this.points, - }; - paths.push(driverPath); - } else if (roles.includes(Role.PASSENGER)) { - const passengerPath: Path = { - key: RouteKey.PASSENGER, - points: [this.points[0], this.points[this.points.length - 1]], - }; - paths.push(passengerPath); - } + 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( @@ -81,11 +42,46 @@ export class Geography { } }; - private setTimezones = (): void => { - this.timezones = this.timezoneFinder.timezones( - this.points[0].lat, - this.points[0].lon, - ); + 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], + ], + }; }; } diff --git a/src/modules/ad/domain/usecases/create-ad.usecase.ts b/src/modules/ad/domain/usecases/create-ad.usecase.ts index 8621933..6fb9d85 100644 --- a/src/modules/ad/domain/usecases/create-ad.usecase.ts +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -15,6 +15,7 @@ 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 'src/modules/geography/domain/entities/coordinate'; @CommandHandler(CreateAdCommand) export class CreateAdUseCase { @@ -38,7 +39,6 @@ export class CreateAdUseCase { private readonly directionEncoder: IEncodeDirection, ) { this.defaultParams = defaultParamsProvider.getParams(); - this.timezone = this.defaultParams.DEFAULT_TIMEZONE; this.georouter = georouterCreator.create( this.defaultParams.GEOROUTER_TYPE, this.defaultParams.GEOROUTER_URL, @@ -48,8 +48,9 @@ export class CreateAdUseCase { 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); - this.setGeography(command.createAdRequest); await this.geography.createRoutes(this.roles, this.georouter, { withDistance: false, withPoints: true, @@ -112,18 +113,24 @@ export class CreateAdUseCase { } } + private setTimezone = (coordinates: Coordinate[]): void => { + this.timezone = this.defaultParams.DEFAULT_TIMEZONE; + try { + const timezones = this.timezoneFinder.timezones( + coordinates[0].lat, + coordinates[0].lon, + ); + 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 = (createAdRequest: CreateAdRequest): void => { - this.geography = new Geography(createAdRequest.waypoints, { - timezone: this.defaultParams.DEFAULT_TIMEZONE, - finder: this.timezoneFinder, - }); - if (this.geography.timezones.length > 0) - this.timezone = this.geography.timezones[0]; + private setGeography = (coordinates: Coordinate[]): void => { + this.geography = new Geography(coordinates); }; } 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 index 00138e4..9c4113d 100644 --- a/src/modules/ad/tests/unit/domain/time-converter.spec.ts +++ b/src/modules/ad/tests/unit/domain/time-converter.spec.ts @@ -15,7 +15,7 @@ describe('TimeConverter', () => { ).toBe(6); }); - it('should return undefined when trying to convert a Europe/Paris datetime to utc datetime without a valid date, time or timezone', () => { + 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(); @@ -26,6 +26,9 @@ describe('TimeConverter', () => { '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'), @@ -36,9 +39,12 @@ describe('TimeConverter', () => { 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-13-01'), + new Date('2023-12-01'), '07:00', 'OlympusMons/Mars', ), diff --git a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts index be96d47..635e966 100644 --- a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts @@ -205,7 +205,6 @@ export abstract class PrismaRepository implements IRepository { const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join( '","', )}") VALUES (${Object.values(fields).join(',')})`; - console.log(command); return await this._prisma.$executeRawUnsafe(command); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) { diff --git a/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts b/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts index 8e249c0..3a85ace 100644 --- a/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts +++ b/src/modules/geography/adapters/secondaries/postgres-direction-encoder.ts @@ -1,8 +1,8 @@ -import { Coordinates } from '../../domain/entities/coordinates'; +import { Coordinate } from '../../domain/entities/coordinate'; import { IEncodeDirection } from '../../domain/interfaces/direction-encoder.interface'; export class PostgresDirectionEncoder implements IEncodeDirection { - encode = (coordinates: Coordinates[]): string => + encode = (coordinates: Coordinate[]): string => [ "'LINESTRING(", coordinates.map((point) => [point.lon, point.lat].join(' ')).join(), diff --git a/src/modules/geography/domain/entities/coordinates.ts b/src/modules/geography/domain/entities/coordinate.ts similarity index 92% rename from src/modules/geography/domain/entities/coordinates.ts rename to src/modules/geography/domain/entities/coordinate.ts index 05769c2..4dd416a 100644 --- a/src/modules/geography/domain/entities/coordinates.ts +++ b/src/modules/geography/domain/entities/coordinate.ts @@ -1,7 +1,7 @@ import { AutoMap } from '@automapper/classes'; import { IsLatitude, IsLongitude, IsNumber } from 'class-validator'; -export class Coordinates { +export class Coordinate { constructor(lon: number, lat: number) { this.lon = lon; this.lat = lat; diff --git a/src/modules/geography/domain/entities/spacetime-point.ts b/src/modules/geography/domain/entities/spacetime-point.ts index 97f130d..7d720e1 100644 --- a/src/modules/geography/domain/entities/spacetime-point.ts +++ b/src/modules/geography/domain/entities/spacetime-point.ts @@ -1,12 +1,12 @@ -import { Coordinates } from './coordinates'; +import { Coordinate } from './coordinate'; export class SpacetimePoint { - coordinates: Coordinates; + coordinate: Coordinate; duration: number; distance: number; - constructor(coordinates: Coordinates, duration: number, distance: number) { - this.coordinates = coordinates; + 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 index 52a5ce1..0f38a49 100644 --- a/src/modules/geography/domain/interfaces/direction-encoder.interface.ts +++ b/src/modules/geography/domain/interfaces/direction-encoder.interface.ts @@ -1,5 +1,5 @@ -import { Coordinates } from '../entities/coordinates'; +import { Coordinate } from '../entities/coordinate'; export interface IEncodeDirection { - encode(coordinates: Coordinates[]): string; + encode(coordinates: Coordinate[]): string; } diff --git a/src/modules/geography/domain/types/point.type.ts b/src/modules/geography/domain/types/point.type.ts index b3733a8..37c49e6 100644 --- a/src/modules/geography/domain/types/point.type.ts +++ b/src/modules/geography/domain/types/point.type.ts @@ -1,6 +1,6 @@ import { PointType } from './point-type.enum'; -import { Coordinates } from '../entities/coordinates'; +import { Coordinate } from '../entities/coordinate'; -export type Point = Coordinates & { +export type Point = Coordinate & { type?: PointType; }; 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/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/matcher/domain/entities/ecosystem/spacetime-point.ts b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts index 91dd7dc..8a45b80 100644 --- a/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts +++ b/src/modules/matcher/domain/entities/ecosystem/spacetime-point.ts @@ -1,12 +1,12 @@ -import { Coordinates } from '../../../../geography/domain/entities/coordinates'; +import { Coordinate } from '../../../../geography/domain/entities/coordinate'; export class SpacetimePoint { - coordinates: Coordinates; + coordinate: Coordinate; duration: number; distance: number; - constructor(coordinates: Coordinates, duration: number, distance: number) { - this.coordinates = coordinates; + constructor(coordinate: Coordinate, duration: number, distance: number) { + this.coordinate = coordinate; this.duration = duration; this.distance = distance; } From 72516037d141530fd296fa7b72a984020866a55b Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 22 May 2023 16:38:34 +0200 Subject: [PATCH 16/19] create ad usecase tests --- .../ad/domain/usecases/create-ad.usecase.ts | 110 ++++++++++-------- .../unit/domain/create-ad.usecase.spec.ts | 84 +++++++++++-- 2 files changed, 132 insertions(+), 62 deletions(-) diff --git a/src/modules/ad/domain/usecases/create-ad.usecase.ts b/src/modules/ad/domain/usecases/create-ad.usecase.ts index 6fb9d85..b04b2f3 100644 --- a/src/modules/ad/domain/usecases/create-ad.usecase.ts +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -56,57 +56,8 @@ export class CreateAdUseCase { withPoints: true, withTime: false, }); - 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; - 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, - ); + this.setAdGeography(command); + this.setAdSchedule(command); return await this.adRepository.createAd(this.ad); } catch (error) { throw error; @@ -133,4 +84,61 @@ export class CreateAdUseCase { 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/tests/unit/domain/create-ad.usecase.spec.ts b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts index 9079642..040c763 100644 --- a/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts +++ b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts @@ -9,14 +9,48 @@ 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 = {}; +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, - points: [], + route: { + points: [], + fwdAzimuth: 0, + backAzimuth: 180, + distance: 20000, + duration: 1800, + }, }, ]), })), @@ -31,7 +65,9 @@ const mockParamsProvider = { const mockTimezoneFinder = { timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), }; -const mockDirectionEncoder = {}; +const mockDirectionEncoder = { + encode: jest.fn(), +}; const createAdRequest: CreateAdRequest = { uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1', @@ -64,6 +100,18 @@ const createAdRequest: CreateAdRequest = { ], }; +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; @@ -103,12 +151,26 @@ describe('CreateAdUseCase', () => { expect(createAdUseCase).toBeDefined(); }); - // describe('execute', () => { - // it('should create an ad', async () => { - // const ad = await createAdUseCase.execute( - // new CreateAdCommand(createAdRequest), - // ); - // expect(ad).toBeInstanceOf(Ad); - // }); - // }); + 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); + }); + }); }); From ca398f43df3779962b2de6f83480b42ed0a54593 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 23 May 2023 14:00:39 +0200 Subject: [PATCH 17/19] integration tests --- .env.test | 7 + prisma/schema.prisma | 1 + .../primaries/ad-messager.controller.ts | 7 +- .../ad/adapters/primaries/ad.presenter.ts | 6 - .../ad/adapters/secondaries/ad.repository.ts | 36 +- .../ad/domain/usecases/create-ad.usecase.ts | 4 +- src/modules/ad/mappers/ad.profile.ts | 2 - .../tests/integration/ad.repository.spec.ts | 402 ++++++++++++++++++ 8 files changed, 440 insertions(+), 25 deletions(-) create mode 100644 .env.test delete mode 100644 src/modules/ad/adapters/primaries/ad.presenter.ts create mode 100644 src/modules/ad/tests/integration/ad.repository.spec.ts 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/prisma/schema.prisma b/prisma/schema.prisma index 2086694..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"] } diff --git a/src/modules/ad/adapters/primaries/ad-messager.controller.ts b/src/modules/ad/adapters/primaries/ad-messager.controller.ts index 9774222..ce10434 100644 --- a/src/modules/ad/adapters/primaries/ad-messager.controller.ts +++ b/src/modules/ad/adapters/primaries/ad-messager.controller.ts @@ -1,6 +1,5 @@ import { RabbitSubscribe } from '@golevelup/nestjs-rabbitmq'; import { Controller } from '@nestjs/common'; -import { Ad } from '../../domain/entities/ad'; import { CommandBus } from '@nestjs/cqrs'; import { CreateAdCommand } from '../../commands/create-ad.command'; import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; @@ -35,12 +34,8 @@ export class AdMessagerController { throw e; } } - const ad: Ad = await this.commandBus.execute( - new CreateAdCommand(createAdRequest), - ); - console.log(ad); + await this.commandBus.execute(new CreateAdCommand(createAdRequest)); } catch (e) { - console.log(e); this.messager.publish( 'logging.matcher.ad.crit', JSON.stringify({ diff --git a/src/modules/ad/adapters/primaries/ad.presenter.ts b/src/modules/ad/adapters/primaries/ad.presenter.ts deleted file mode 100644 index 1a4d67c..0000000 --- a/src/modules/ad/adapters/primaries/ad.presenter.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AutoMap } from '@automapper/classes'; - -export class AdPresenter { - @AutoMap() - uuid: string; -} diff --git a/src/modules/ad/adapters/secondaries/ad.repository.ts b/src/modules/ad/adapters/secondaries/ad.repository.ts index 0c83052..ea580c2 100644 --- a/src/modules/ad/adapters/secondaries/ad.repository.ts +++ b/src/modules/ad/adapters/secondaries/ad.repository.ts @@ -28,28 +28,46 @@ export class AdRepository extends MatcherRepository { driver: ad.driver ? 'true' : 'false', passenger: ad.passenger ? 'true' : 'false', frequency: `'${ad.frequency}'`, - fromDate: `'${ad.fromDate.getFullYear()}-${ad.fromDate.getMonth()}-${ad.fromDate.getDate()}'`, - toDate: `'${ad.toDate.getFullYear()}-${ad.toDate.getMonth()}-${ad.toDate.getDate()}'`, + 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()}-${ad.monTime.getDate()}T${ad.monTime.getHours()}:${ad.monTime.getMinutes()}Z'` + ? `'${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()}-${ad.tueTime.getDate()}T${ad.tueTime.getHours()}:${ad.tueTime.getMinutes()}Z'` + ? `'${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()}-${ad.wedTime.getDate()}T${ad.wedTime.getHours()}:${ad.wedTime.getMinutes()}Z'` + ? `'${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()}-${ad.thuTime.getDate()}T${ad.thuTime.getHours()}:${ad.thuTime.getMinutes()}Z'` + ? `'${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()}-${ad.friTime.getDate()}T${ad.friTime.getHours()}:${ad.friTime.getMinutes()}Z'` + ? `'${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()}-${ad.satTime.getDate()}T${ad.satTime.getHours()}:${ad.satTime.getMinutes()}Z'` + ? `'${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()}-${ad.sunTime.getDate()}T${ad.sunTime.getHours()}:${ad.sunTime.getMinutes()}Z'` + ? `'${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, diff --git a/src/modules/ad/domain/usecases/create-ad.usecase.ts b/src/modules/ad/domain/usecases/create-ad.usecase.ts index b04b2f3..71fcc98 100644 --- a/src/modules/ad/domain/usecases/create-ad.usecase.ts +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -15,7 +15,7 @@ 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 'src/modules/geography/domain/entities/coordinate'; +import { Coordinate } from '../../../geography/domain/entities/coordinate'; @CommandHandler(CreateAdCommand) export class CreateAdUseCase { @@ -68,8 +68,8 @@ export class CreateAdUseCase { this.timezone = this.defaultParams.DEFAULT_TIMEZONE; try { const timezones = this.timezoneFinder.timezones( - coordinates[0].lat, coordinates[0].lon, + coordinates[0].lat, ); if (timezones.length > 0) this.timezone = timezones[0]; } catch (e) {} diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index 7b7de92..bbc38d5 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -2,7 +2,6 @@ import { createMap, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; -import { AdPresenter } from '../adapters/primaries/ad.presenter'; import { CreateAdRequest } from '../domain/dtos/create-ad.request'; @Injectable() @@ -13,7 +12,6 @@ export class AdProfile extends AutomapperProfile { override get profile() { return (mapper: any) => { - createMap(mapper, Ad, AdPresenter); 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'); + }); + }); +}); From ec8cf87d2928091e506d2b8ef95d0f0a54a191c5 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 23 May 2023 14:01:50 +0200 Subject: [PATCH 18/19] integration tests --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dfca5ef..15b60fc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ test: - docker-compose -f docker-compose.ci.tools.yml -p matcher-tools --env-file ci/.env.ci up -d - sh ci/wait-up.sh - docker-compose -f docker-compose.ci.service.yml -p matcher-service --env-file ci/.env.ci up -d - # - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci" + - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci" coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ rules: - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' From 2cd1155919acff8c9406b9874c8fc61757859b59 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 23 May 2023 16:04:52 +0200 Subject: [PATCH 19/19] suspend ci integration tests --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 15b60fc..dfca5ef 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ test: - docker-compose -f docker-compose.ci.tools.yml -p matcher-tools --env-file ci/.env.ci up -d - sh ci/wait-up.sh - docker-compose -f docker-compose.ci.service.yml -p matcher-service --env-file ci/.env.ci up -d - - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci" + # - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci" coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/ rules: - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'