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; } }