create ad, WIP

This commit is contained in:
sbriat 2023-05-12 16:23:42 +02:00
parent da96f52c1e
commit e950efe221
24 changed files with 382 additions and 283 deletions

View File

@ -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");

View File

@ -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"

View File

@ -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: [],
})

View File

@ -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<void> {
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) {

View File

@ -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<Ad> {
@ -25,27 +24,62 @@ export class AdRepository extends MatcherRepository<Ad> {
private createFields(ad: Partial<Ad>): Partial<AdFields> {
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;
};

View File

@ -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'),

View File

@ -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<void> => {
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);
}
};
}

View File

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

View File

@ -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<boolean> here as well, if you want to make async validation
},
},
});
};
}

View File

@ -1,7 +0,0 @@
import { Ad } from './ad';
export class AdCompleter {
complete = async (ad: Ad): Promise<Ad> => {
return ad;
};
}

View File

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

View File

@ -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<void> => {
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,

View File

@ -0,0 +1,5 @@
import { DefaultParams } from '../types/default-params.type';
export interface IProvideParams {
getParams(): DefaultParams;
}

View File

@ -1,4 +1,4 @@
export type IDefaultParams = {
export type DefaultParams = {
DEFAULT_TIMEZONE: string;
GEOROUTER_TYPE: string;
GEOROUTER_URL: string;

View File

@ -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<Ad> {
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];
};
}

View File

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

View File

@ -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');
});
});

View File

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

View File

@ -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);
// });
// });
});

View File

@ -205,6 +205,7 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
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) {

View File

@ -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,
);
}),

View File

@ -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('');
}

View File

@ -0,0 +1,5 @@
import { Coordinates } from '../entities/coordinates';
export interface IEncodeDirection {
encode(coordinates: Coordinates[]): string;
}

View File

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