refactor to ddh, first commit

This commit is contained in:
sbriat
2023-08-16 12:28:20 +02:00
parent 0a6e4c0bf6
commit ce48890a66
208 changed files with 2596 additions and 2052 deletions

View File

@@ -0,0 +1,140 @@
import { AutoMap } from '@automapper/classes';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsMilitaryTime,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
import { Frequency } from '../types/frequency.enum';
import { Coordinate } from '../../../geography/domain/entities/coordinate';
import { Type } from 'class-transformer';
import { HasTruthyWith } from './has-truthy-with.validator';
export class CreateAdRequest {
@IsString()
@IsNotEmpty()
@AutoMap()
uuid: string;
@IsString()
@IsNotEmpty()
@AutoMap()
userUuid: string;
@HasTruthyWith('passenger', {
message: 'A role (driver or passenger) must be set to true',
})
@IsBoolean()
@AutoMap()
driver: boolean;
@IsBoolean()
@AutoMap()
passenger: boolean;
@IsEnum(Frequency)
@AutoMap()
frequency: Frequency;
@Type(() => Date)
@IsDate()
@AutoMap()
fromDate: Date;
@Type(() => Date)
@IsDate()
@AutoMap()
toDate: Date;
@IsOptional()
@IsMilitaryTime()
@AutoMap()
monTime?: string;
@IsOptional()
@IsMilitaryTime()
@AutoMap()
tueTime?: string;
@IsOptional()
@IsMilitaryTime()
@AutoMap()
wedTime?: string;
@IsOptional()
@IsMilitaryTime()
@AutoMap()
thuTime?: string;
@IsOptional()
@IsMilitaryTime()
@AutoMap()
friTime?: string;
@IsOptional()
@IsMilitaryTime()
@AutoMap()
satTime?: string;
@IsOptional()
@IsMilitaryTime()
@AutoMap()
sunTime?: string;
@IsNumber()
@AutoMap()
monMargin: number;
@IsNumber()
@AutoMap()
tueMargin: number;
@IsNumber()
@AutoMap()
wedMargin: number;
@IsNumber()
@AutoMap()
thuMargin: number;
@IsNumber()
@AutoMap()
friMargin: number;
@IsNumber()
@AutoMap()
satMargin: number;
@IsNumber()
@AutoMap()
sunMargin: number;
@Type(() => Coordinate)
@IsArray()
@ArrayMinSize(2)
@AutoMap(() => [Coordinate])
addresses: Coordinate[];
@IsNumber()
@AutoMap()
seatsDriver: number;
@IsNumber()
@AutoMap()
seatsPassenger: number;
@IsOptional()
@IsNumber()
@AutoMap()
seatsUsed?: number;
@IsBoolean()
@AutoMap()
strict: boolean;
}

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

@@ -0,0 +1,109 @@
import { AutoMap } from '@automapper/classes';
import { Frequency } from '../types/frequency.enum';
export class Ad {
@AutoMap()
uuid: string;
@AutoMap()
userUuid: string;
@AutoMap()
driver: boolean;
@AutoMap()
passenger: boolean;
@AutoMap()
frequency: Frequency;
@AutoMap()
fromDate: Date;
@AutoMap()
toDate: Date;
@AutoMap()
monTime: Date;
@AutoMap()
tueTime: Date;
@AutoMap()
wedTime: Date;
@AutoMap()
thuTime: Date;
@AutoMap()
friTime: Date;
@AutoMap()
satTime: Date;
@AutoMap()
sunTime: Date;
@AutoMap()
monMargin: number;
@AutoMap()
tueMargin: number;
@AutoMap()
wedMargin: number;
@AutoMap()
thuMargin: number;
@AutoMap()
friMargin: number;
@AutoMap()
satMargin: number;
@AutoMap()
sunMargin: number;
@AutoMap()
driverDuration?: number;
@AutoMap()
driverDistance?: number;
@AutoMap()
passengerDuration?: number;
@AutoMap()
passengerDistance?: number;
@AutoMap()
waypoints: string;
@AutoMap()
direction: string;
@AutoMap()
fwdAzimuth: number;
@AutoMap()
backAzimuth: number;
@AutoMap()
seatsDriver: number;
@AutoMap()
seatsPassenger: number;
@AutoMap()
seatsUsed: number;
@AutoMap()
strict: boolean;
@AutoMap()
createdAt: Date;
@AutoMap()
updatedAt: Date;
}

View File

@@ -0,0 +1,92 @@
import { Coordinate } from '../../../geography/domain/entities/coordinate';
import { Route } from '../../../geography/domain/entities/route';
import { Role } from '../types/role.enum';
import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface';
import { Path } from '../../../geography/domain/types/path.type';
import { GeorouterSettings } from '../../../geography/domain/types/georouter-settings.type';
export class Geography {
private coordinates: Coordinate[];
driverRoute: Route;
passengerRoute: Route;
constructor(coordinates: Coordinate[]) {
this.coordinates = coordinates;
}
createRoutes = async (
roles: Role[],
georouter: IGeorouter,
settings: GeorouterSettings,
): Promise<void> => {
const paths: Path[] = this.getPaths(roles);
const routes = await georouter.route(paths, settings);
if (routes.some((route) => route.key == RouteType.COMMON)) {
this.driverRoute = routes.find(
(route) => route.key == RouteType.COMMON,
).route;
this.passengerRoute = routes.find(
(route) => route.key == RouteType.COMMON,
).route;
} else {
if (routes.some((route) => route.key == RouteType.DRIVER)) {
this.driverRoute = routes.find(
(route) => route.key == RouteType.DRIVER,
).route;
}
if (routes.some((route) => route.key == RouteType.PASSENGER)) {
this.passengerRoute = routes.find(
(route) => route.key == RouteType.PASSENGER,
).route;
}
}
};
private getPaths = (roles: Role[]): Path[] => {
const paths: Path[] = [];
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
if (this.coordinates.length == 2) {
// 2 points => same route for driver and passenger
const commonPath: Path = {
key: RouteType.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: RouteType.DRIVER,
points: this.coordinates,
};
};
private createPassengerPath = (): Path => {
return {
key: RouteType.PASSENGER,
points: [
this.coordinates[0],
this.coordinates[this.coordinates.length - 1],
],
};
};
}
export enum RouteType {
COMMON = 'common',
DRIVER = 'driver',
PASSENGER = 'passenger',
}

View File

@@ -0,0 +1,19 @@
import { DateTime, TimeZone } from 'timezonecomplete';
export class TimeConverter {
static toUtcDatetime = (date: Date, time: string, timezone: string): Date => {
try {
if (!date || !time || !timezone) throw new Error();
return new Date(
new DateTime(
`${date.toISOString().split('T')[0]}T${time}:00`,
TimeZone.zone(timezone, false),
)
.convert(TimeZone.zone('UTC'))
.toIsoString(),
);
} catch (e) {
return undefined;
}
};
}

View File

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

View File

@@ -0,0 +1,5 @@
export type DefaultParams = {
DEFAULT_TIMEZONE: string;
GEOROUTER_TYPE: string;
GEOROUTER_URL: string;
};

View File

@@ -0,0 +1,4 @@
export enum Frequency {
PUNCTUAL = 'PUNCTUAL',
RECURRENT = 'RECURRENT',
}

View File

@@ -0,0 +1,4 @@
export enum Role {
DRIVER = 'DRIVER',
PASSENGER = 'PASSENGER',
}

View File

@@ -0,0 +1,150 @@
import { CommandHandler } from '@nestjs/cqrs';
import { CreateAdCommand } from '../../commands/create-ad.command';
import { Ad } from '../entities/ad';
import { AdRepository } from '../../adapters/secondaries/ad.repository';
import { InjectMapper } from '@automapper/nestjs';
import { Mapper } from '@automapper/core';
import { CreateAdRequest } from '../dtos/create-ad.request';
import { Inject } from '@nestjs/common';
import { IProvideParams } from '../interfaces/params-provider.interface';
import { ICreateGeorouter } from '../../../geography/domain/interfaces/georouter-creator.interface';
import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface';
import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface';
import { DefaultParams } from '../types/default-params.type';
import { Role } from '../types/role.enum';
import { Geography } from '../entities/geography';
import { IEncodeDirection } from '../../../geography/domain/interfaces/direction-encoder.interface';
import { TimeConverter } from '../entities/time-converter';
import { Coordinate } from '../../../geography/domain/entities/coordinate';
import {
DIRECTION_ENCODER,
GEOROUTER_CREATOR,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
} from '../../ad.constants';
@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(PARAMS_PROVIDER)
private readonly defaultParamsProvider: IProvideParams,
@Inject(GEOROUTER_CREATOR)
private readonly georouterCreator: ICreateGeorouter,
@Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: IFindTimezone,
@Inject(DIRECTION_ENCODER)
private readonly directionEncoder: IEncodeDirection,
) {
this.defaultParams = defaultParamsProvider.getParams();
this.georouter = georouterCreator.create(
this.defaultParams.GEOROUTER_TYPE,
this.defaultParams.GEOROUTER_URL,
);
}
async execute(command: CreateAdCommand): Promise<Ad> {
try {
this.ad = this.mapper.map(command.createAdRequest, CreateAdRequest, Ad);
this.setTimezone(command.createAdRequest.addresses);
this.setGeography(command.createAdRequest.addresses);
this.setRoles(command.createAdRequest);
await this.geography.createRoutes(this.roles, this.georouter, {
withDistance: false,
withPoints: true,
withTime: false,
});
this.setAdGeography(command);
this.setAdSchedule(command);
return await this.adRepository.createAd(this.ad);
} catch (error) {
throw error;
}
}
private setTimezone = (coordinates: Coordinate[]): void => {
this.timezone = this.defaultParams.DEFAULT_TIMEZONE;
try {
const timezones = this.timezoneFinder.timezones(
coordinates[0].lon,
coordinates[0].lat,
);
if (timezones.length > 0) this.timezone = timezones[0];
} catch (e) {}
};
private setRoles = (createAdRequest: CreateAdRequest): void => {
this.roles = [];
if (createAdRequest.driver) this.roles.push(Role.DRIVER);
if (createAdRequest.passenger) this.roles.push(Role.PASSENGER);
};
private setGeography = (coordinates: Coordinate[]): void => {
this.geography = new Geography(coordinates);
};
private setAdGeography = (command: CreateAdCommand): void => {
this.ad.driverDistance = this.geography.driverRoute?.distance;
this.ad.driverDuration = this.geography.driverRoute?.duration;
this.ad.passengerDistance = this.geography.passengerRoute?.distance;
this.ad.passengerDuration = this.geography.passengerRoute?.duration;
this.ad.fwdAzimuth = this.geography.driverRoute
? this.geography.driverRoute.fwdAzimuth
: this.geography.passengerRoute.fwdAzimuth;
this.ad.backAzimuth = this.geography.driverRoute
? this.geography.driverRoute.backAzimuth
: this.geography.passengerRoute.backAzimuth;
this.ad.waypoints = this.directionEncoder.encode(
command.createAdRequest.addresses,
);
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,
);
};
}