From a98e5b3c83f33af0c237764ac814a032289cd5db Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 25 Aug 2023 09:53:41 +0200 Subject: [PATCH 01/52] basic match query --- .../ad/interface/message-handlers/ad.types.ts | 1 - .../application/queries/match/match.query.ts | 27 ++++ .../matcher/core/application/types/address.ts | 10 ++ .../core/application/types/coordinates.ts | 4 + .../core/application/types/schedule-item.ts | 5 + .../core/application/types/waypoint.ts | 5 + .../matcher/core/domain/match.types.ts | 8 ++ .../dtos/match.paginated.response.dto.ts | 6 + .../interface/dtos/match.response.dto.ts | 3 + .../grpc-controllers/dtos/address.dto.ts | 26 ++++ .../grpc-controllers/dtos/coordinates.dto.ts | 9 ++ .../dtos/match.request.dto.ts | 119 ++++++++++++++++++ .../dtos/schedule-item.dto.ts | 16 +++ .../decorators/has-day.decorator.ts | 34 +++++ .../has-valid-position-indexes.decorator.ts | 22 ++++ .../decorators/is-after-or-equal.decorator.ts | 43 +++++++ .../has-valid-position-indexes.validator.ts | 17 +++ .../grpc-controllers/dtos/waypoint.dto.ts | 8 ++ .../grpc-controllers/match.grpc-controller.ts | 37 ++++++ .../unit/interface/has-day.decorator.spec.ts | 60 +++++++++ ...s-valid-position-indexes.decorator.spec.ts | 62 +++++++++ ...s-valid-position-indexes.validator.spec.ts | 75 +++++++++++ .../is-after-or-equal.decorator.spec.ts | 45 +++++++ .../interface/match.grpc.controller.spec.ts | 82 ++++++++++++ 24 files changed, 723 insertions(+), 1 deletion(-) create mode 100644 src/modules/matcher/core/application/queries/match/match.query.ts create mode 100644 src/modules/matcher/core/application/types/address.ts create mode 100644 src/modules/matcher/core/application/types/coordinates.ts create mode 100644 src/modules/matcher/core/application/types/schedule-item.ts create mode 100644 src/modules/matcher/core/application/types/waypoint.ts create mode 100644 src/modules/matcher/core/domain/match.types.ts create mode 100644 src/modules/matcher/interface/dtos/match.paginated.response.dto.ts create mode 100644 src/modules/matcher/interface/dtos/match.response.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts create mode 100644 src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts create mode 100644 src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts create mode 100644 src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts diff --git a/src/modules/ad/interface/message-handlers/ad.types.ts b/src/modules/ad/interface/message-handlers/ad.types.ts index 1deb105..5bc7e88 100644 --- a/src/modules/ad/interface/message-handlers/ad.types.ts +++ b/src/modules/ad/interface/message-handlers/ad.types.ts @@ -2,7 +2,6 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; export type Ad = { id: string; - userId: string; driver: boolean; passenger: boolean; frequency: Frequency; diff --git a/src/modules/matcher/core/application/queries/match/match.query.ts b/src/modules/matcher/core/application/queries/match/match.query.ts new file mode 100644 index 0000000..73531c7 --- /dev/null +++ b/src/modules/matcher/core/application/queries/match/match.query.ts @@ -0,0 +1,27 @@ +import { QueryBase } from '@mobicoop/ddd-library'; +import { Frequency } from '@modules/matcher/core/domain/match.types'; +import { ScheduleItem } from '../../types/schedule-item'; +import { Waypoint } from '../../types/waypoint'; + +export class MatchQuery extends QueryBase { + readonly driver?: boolean; + readonly passenger?: boolean; + readonly frequency?: Frequency; + readonly fromDate: string; + readonly toDate: string; + readonly schedule: ScheduleItem[]; + readonly strict?: boolean; + readonly waypoints: Waypoint[]; + + constructor(props: MatchQuery) { + super(); + this.driver = props.driver; + this.passenger = props.passenger; + this.frequency = props.frequency; + this.fromDate = props.fromDate; + this.toDate = props.toDate; + this.schedule = props.schedule; + this.strict = props.strict; + this.waypoints = props.waypoints; + } +} diff --git a/src/modules/matcher/core/application/types/address.ts b/src/modules/matcher/core/application/types/address.ts new file mode 100644 index 0000000..bdf8e6b --- /dev/null +++ b/src/modules/matcher/core/application/types/address.ts @@ -0,0 +1,10 @@ +import { Coordinates } from './coordinates'; + +export type Address = { + name?: string; + houseNumber?: string; + street?: string; + locality?: string; + postalCode?: string; + country: string; +} & Coordinates; diff --git a/src/modules/matcher/core/application/types/coordinates.ts b/src/modules/matcher/core/application/types/coordinates.ts new file mode 100644 index 0000000..8e149ed --- /dev/null +++ b/src/modules/matcher/core/application/types/coordinates.ts @@ -0,0 +1,4 @@ +export type Coordinates = { + lon: number; + lat: number; +}; diff --git a/src/modules/matcher/core/application/types/schedule-item.ts b/src/modules/matcher/core/application/types/schedule-item.ts new file mode 100644 index 0000000..a40e06d --- /dev/null +++ b/src/modules/matcher/core/application/types/schedule-item.ts @@ -0,0 +1,5 @@ +export type ScheduleItem = { + day?: number; + time: string; + margin?: number; +}; diff --git a/src/modules/matcher/core/application/types/waypoint.ts b/src/modules/matcher/core/application/types/waypoint.ts new file mode 100644 index 0000000..c73e024 --- /dev/null +++ b/src/modules/matcher/core/application/types/waypoint.ts @@ -0,0 +1,5 @@ +import { Address } from './address'; + +export type Waypoint = { + position?: number; +} & Address; diff --git a/src/modules/matcher/core/domain/match.types.ts b/src/modules/matcher/core/domain/match.types.ts new file mode 100644 index 0000000..89be65e --- /dev/null +++ b/src/modules/matcher/core/domain/match.types.ts @@ -0,0 +1,8 @@ +export enum Frequency { + PUNCTUAL = 'PUNCTUAL', + RECURRENT = 'RECURRENT', +} + +export enum AlgorithmType { + CLASSIC = 'CLASSIC', +} diff --git a/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts b/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts new file mode 100644 index 0000000..52f75c8 --- /dev/null +++ b/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts @@ -0,0 +1,6 @@ +import { PaginatedResponseDto } from '@mobicoop/ddd-library'; +import { MatchResponseDto } from './match.response.dto'; + +export class MatchPaginatedResponseDto extends PaginatedResponseDto { + readonly data: readonly MatchResponseDto[]; +} diff --git a/src/modules/matcher/interface/dtos/match.response.dto.ts b/src/modules/matcher/interface/dtos/match.response.dto.ts new file mode 100644 index 0000000..a69fc89 --- /dev/null +++ b/src/modules/matcher/interface/dtos/match.response.dto.ts @@ -0,0 +1,3 @@ +export class MatchResponseDto { + adId: string; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts new file mode 100644 index 0000000..9659d96 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts @@ -0,0 +1,26 @@ +import { IsOptional, IsString } from 'class-validator'; +import { CoordinatesDto as CoordinatesDto } from './coordinates.dto'; + +export class AddressDto extends CoordinatesDto { + @IsOptional() + name?: string; + + @IsOptional() + @IsString() + houseNumber?: string; + + @IsOptional() + @IsString() + street?: string; + + @IsOptional() + @IsString() + locality?: string; + + @IsOptional() + @IsString() + postalCode?: string; + + @IsString() + country: string; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts new file mode 100644 index 0000000..cb636ae --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts @@ -0,0 +1,9 @@ +import { IsLatitude, IsLongitude } from 'class-validator'; + +export class CoordinatesDto { + @IsLongitude() + lon: number; + + @IsLatitude() + lat: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts new file mode 100644 index 0000000..607d485 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts @@ -0,0 +1,119 @@ +import { + AlgorithmType, + Frequency, +} from '@modules/matcher/core/domain/match.types'; +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsDecimal, + IsEnum, + IsISO8601, + IsInt, + IsOptional, + Max, + Min, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { HasDay } from './validators/decorators/has-day.decorator'; +import { IsAfterOrEqual } from './validators/decorators/is-after-or-equal.decorator'; +import { ScheduleItemDto } from './schedule-item.dto'; +import { WaypointDto } from './waypoint.dto'; +import { HasValidPositionIndexes } from './validators/decorators/has-valid-position-indexes.decorator'; + +export class MatchRequestDto { + @IsOptional() + @IsBoolean() + driver?: boolean; + + @IsOptional() + @IsBoolean() + passenger?: boolean; + + @IsEnum(Frequency) + @HasDay('schedule', { + message: 'At least a day is required for a recurrent ad', + }) + frequency: Frequency; + + @IsISO8601({ + strict: true, + strictSeparator: true, + }) + fromDate: string; + + @IsISO8601({ + strict: true, + strictSeparator: true, + }) + @IsAfterOrEqual('fromDate', { + message: 'toDate must be after or equal to fromDate', + }) + toDate: string; + + @Type(() => ScheduleItemDto) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + schedule: ScheduleItemDto[]; + + @IsOptional() + @IsInt() + seatsProposed?: number; + + @IsOptional() + @IsInt() + seatsRequested?: number; + + @IsOptional() + @IsBoolean() + strict?: boolean; + + @Type(() => WaypointDto) + @IsArray() + @ArrayMinSize(2) + @HasValidPositionIndexes() + @ValidateNested({ each: true }) + waypoints: WaypointDto[]; + + @IsOptional() + @IsEnum(AlgorithmType) + algorithm?: AlgorithmType; + + @IsOptional() + @IsInt() + remoteness?: number; + + @IsOptional() + @IsBoolean() + useProportion?: boolean; + + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + proportion?: number; + + @IsOptional() + @IsBoolean() + useAzimuth?: boolean; + + @IsOptional() + @IsInt() + @Min(0) + @Max(359) + azimuthMargin?: number; + + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + maxDetourDistanceRatio?: number; + + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + maxDetourDurationRatio?: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts new file mode 100644 index 0000000..112adc2 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts @@ -0,0 +1,16 @@ +import { IsOptional, IsMilitaryTime, IsInt, Min, Max } from 'class-validator'; + +export class ScheduleItemDto { + @IsOptional() + @IsInt() + @Min(0) + @Max(6) + day?: number; + + @IsMilitaryTime() + time: string; + + @IsOptional() + @IsInt() + margin?: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts new file mode 100644 index 0000000..ed3cf0f --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts @@ -0,0 +1,34 @@ +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { + registerDecorator, + ValidationOptions, + ValidationArguments, +} from 'class-validator'; + +export function HasDay( + property: string, + validationOptions?: ValidationOptions, +) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'hasDay', + 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 ( + value == Frequency.PUNCTUAL || + (Array.isArray(relatedValue) && + relatedValue.some((scheduleItem) => + scheduleItem.hasOwnProperty('day'), + )) + ); + }, + }, + }); + }; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts new file mode 100644 index 0000000..87e3a36 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts @@ -0,0 +1,22 @@ +import { ValidateBy, ValidationOptions, buildMessage } from 'class-validator'; +import { hasValidPositionIndexes } from '../has-valid-position-indexes.validator'; +import { WaypointDto } from '../../waypoint.dto'; + +export const HasValidPositionIndexes = ( + validationOptions?: ValidationOptions, +): PropertyDecorator => + ValidateBy( + { + name: '', + constraints: [], + validator: { + validate: (waypoints: WaypointDto[]): boolean => + hasValidPositionIndexes(waypoints), + defaultMessage: buildMessage( + () => `invalid waypoints positions`, + validationOptions, + ), + }, + }, + validationOptions, + ); diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts new file mode 100644 index 0000000..fb6e734 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts @@ -0,0 +1,43 @@ +import { + registerDecorator, + ValidationOptions, + ValidationArguments, + isISO8601, +} from 'class-validator'; + +export function IsAfterOrEqual( + property: string, + validationOptions?: ValidationOptions, +) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: 'isAfterOrEqual', + 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]; + if ( + !( + typeof value === 'string' && + typeof relatedValue === 'string' && + isISO8601(value, { + strict: true, + strictSeparator: true, + }) && + isISO8601(relatedValue, { + strict: true, + strictSeparator: true, + }) + ) + ) + return false; + return new Date(value) >= new Date(relatedValue); + }, + }, + }); + }; +} diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts new file mode 100644 index 0000000..04504a8 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts @@ -0,0 +1,17 @@ +import { WaypointDto } from '../waypoint.dto'; + +export const hasValidPositionIndexes = (waypoints: WaypointDto[]): boolean => { + if (!waypoints) return false; + if (waypoints.length == 0) return false; + if (waypoints.every((waypoint) => waypoint.position === undefined)) + return false; + if (waypoints.every((waypoint) => typeof waypoint.position === 'number')) { + const positions = Array.from(waypoints, (waypoint) => waypoint.position); + positions.sort(); + for (let i = 1; i < positions.length; i++) + if (positions[i] != positions[i - 1] + 1) return false; + + return true; + } + return false; +}; diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts new file mode 100644 index 0000000..ded5386 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts @@ -0,0 +1,8 @@ +import { IsInt, IsOptional } from 'class-validator'; +import { AddressDto } from './address.dto'; + +export class WaypointDto extends AddressDto { + @IsOptional() + @IsInt() + position?: number; +} diff --git a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts new file mode 100644 index 0000000..7fdee64 --- /dev/null +++ b/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts @@ -0,0 +1,37 @@ +import { Controller, UsePipes } from '@nestjs/common'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { RpcValidationPipe } from '@mobicoop/ddd-library'; +import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; +import { QueryBus } from '@nestjs/cqrs'; +import { MatchRequestDto } from './dtos/match.request.dto'; +import { MatchQuery } from '@modules/matcher/core/application/queries/match/match.query'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: false, + forbidUnknownValues: false, + }), +) +@Controller() +export class MatchGrpcController { + constructor(private readonly queryBus: QueryBus) {} + + @GrpcMethod('MatcherService', 'Match') + async match(data: MatchRequestDto): Promise { + try { + const matches = await this.queryBus.execute(new MatchQuery(data)); + return { + data: matches, + page: 1, + perPage: 5, + total: 1, + }; + } catch (e) { + throw new RpcException({ + code: RpcExceptionCode.UNKNOWN, + message: e.message, + }); + } + } +} diff --git a/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts b/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts new file mode 100644 index 0000000..50c29e8 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts @@ -0,0 +1,60 @@ +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { ScheduleItemDto } from '@modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto'; +import { HasDay } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator'; +import { Validator } from 'class-validator'; + +describe('Has day decorator', () => { + class MyClass { + @HasDay('schedule', { + message: 'At least a day is required for a recurrent ad', + }) + frequency: Frequency; + + schedule: ScheduleItemDto[]; + } + + it('should return a property decorator has a function', () => { + const hasDay = HasDay('someProperty'); + expect(typeof hasDay).toBe('function'); + }); + + it('should validate a punctual frequency associated with a valid schedule', async () => { + const myClassInstance = new MyClass(); + myClassInstance.frequency = Frequency.PUNCTUAL; + myClassInstance.schedule = [ + { + time: '07:15', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + + it('should validate a recurrent frequency associated with a valid schedule', async () => { + const myClassInstance = new MyClass(); + myClassInstance.frequency = Frequency.RECURRENT; + myClassInstance.schedule = [ + { + time: '07:15', + day: 1, + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + + it('should not validate a recurrent frequency associated with an invalid schedule', async () => { + const myClassInstance = new MyClass(); + myClassInstance.frequency = Frequency.RECURRENT; + myClassInstance.schedule = [ + { + time: '07:15', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts new file mode 100644 index 0000000..54fb8d1 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts @@ -0,0 +1,62 @@ +import { HasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator'; +import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { Validator } from 'class-validator'; + +describe('valid position indexes decorator', () => { + class MyClass { + @HasValidPositionIndexes() + waypoints: WaypointDto[]; + } + it('should return a property decorator has a function', () => { + const hasValidPositionIndexes = HasValidPositionIndexes(); + expect(typeof hasValidPositionIndexes).toBe('function'); + }); + it('should validate an array of waypoints with valid positions', async () => { + const myClassInstance = new MyClass(); + myClassInstance.waypoints = [ + { + position: 0, + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }, + { + position: 1, + lon: 49.2628, + lat: 4.0347, + locality: 'Reims', + postalCode: '51454', + country: 'France', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + it('should not validate an array of waypoints with invalid positions', async () => { + const myClassInstance = new MyClass(); + myClassInstance.waypoints = [ + { + position: 1, + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }, + { + position: 1, + lon: 49.2628, + lat: 4.0347, + locality: 'Reims', + postalCode: '51454', + country: 'France', + }, + ]; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts new file mode 100644 index 0000000..a0c0661 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts @@ -0,0 +1,75 @@ +import { hasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator'; +import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; + +describe('addresses position validator', () => { + const mockAddress1: WaypointDto = { + lon: 48.689445, + lat: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', + }; + const mockAddress2: WaypointDto = { + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }; + const mockAddress3: WaypointDto = { + lon: 49.2628, + lat: 4.0347, + locality: 'Reims', + postalCode: '51454', + country: 'France', + }; + + it('should not validate if no position is defined', () => { + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should not validate if only one position is defined', () => { + mockAddress1.position = 0; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should not validate if positions are partially defined', () => { + mockAddress1.position = 0; + mockAddress2.position = null; + mockAddress3.position = undefined; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + + it('should not validate if multiple positions have same value', () => { + mockAddress1.position = 0; + mockAddress2.position = 1; + mockAddress3.position = 1; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should validate if all positions are ordered', () => { + mockAddress1.position = 0; + mockAddress2.position = 1; + mockAddress3.position = 2; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeTruthy(); + mockAddress1.position = 1; + mockAddress2.position = 2; + mockAddress3.position = 3; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeTruthy(); + }); + it('should not validate if no waypoints are defined', () => { + expect(hasValidPositionIndexes(undefined)).toBeFalsy(); + expect(hasValidPositionIndexes([])).toBeFalsy(); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts b/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts new file mode 100644 index 0000000..b388a2c --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts @@ -0,0 +1,45 @@ +import { IsAfterOrEqual } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator'; +import { Validator } from 'class-validator'; + +describe('Is after or equal decorator', () => { + class MyClass { + firstDate: string; + + @IsAfterOrEqual('firstDate', { + message: 'secondDate must be after or equal to firstDate', + }) + secondDate: string; + } + + it('should return a property decorator has a function', () => { + const isAfterOrEqual = IsAfterOrEqual('someProperty'); + expect(typeof isAfterOrEqual).toBe('function'); + }); + + it('should validate a secondDate posterior to firstDate', async () => { + const myClassInstance = new MyClass(); + myClassInstance.firstDate = '2023-07-20'; + myClassInstance.secondDate = '2023-07-30'; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(0); + }); + + it('should not validate a secondDate prior to firstDate', async () => { + const myClassInstance = new MyClass(); + myClassInstance.firstDate = '2023-07-20'; + myClassInstance.secondDate = '2023-07-19'; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); + + it('should not validate if dates are invalid', async () => { + const myClassInstance = new MyClass(); + myClassInstance.firstDate = '2023-07-40'; + myClassInstance.secondDate = '2023-07-19'; + const validator = new Validator(); + const validation = await validator.validate(myClassInstance); + expect(validation.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts new file mode 100644 index 0000000..7c1ef68 --- /dev/null +++ b/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts @@ -0,0 +1,82 @@ +import { Frequency } from '@modules/matcher/core/domain/match.types'; +import { MatchRequestDto } from '@modules/matcher/interface/grpc-controllers/dtos/match.request.dto'; +import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { MatchGrpcController } from '@modules/matcher/interface/grpc-controllers/match.grpc-controller'; +import { QueryBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; + +const originWaypoint: WaypointDto = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: WaypointDto = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const punctualMatchRequestDto: MatchRequestDto = { + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-15', + toDate: '2023-08-15', + schedule: [ + { + time: '07:00', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], +}; + +const mockQueryBus = { + execute: jest.fn().mockImplementation(() => [ + { + adId: 1, + }, + { + adId: 2, + }, + ]), +}; + +describe('Match Grpc Controller', () => { + let matchGrpcController: MatchGrpcController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MatchGrpcController, + { + provide: QueryBus, + useValue: mockQueryBus, + }, + ], + }).compile(); + + matchGrpcController = module.get(MatchGrpcController); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(matchGrpcController).toBeDefined(); + }); + + it('should return matches', async () => { + const matchPaginatedResponseDto = await matchGrpcController.match( + punctualMatchRequestDto, + ); + expect(matchPaginatedResponseDto.data).toHaveLength(2); + }); +}); From effe51b9a201a20480776aa65127aa1a874ec1cb Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 25 Aug 2023 10:37:32 +0200 Subject: [PATCH 02/52] basic match query --- .../dtos/match.request.dto.ts | 8 ++++ .../grpc-controllers/match.grpc-controller.ts | 2 +- .../interface/match.grpc.controller.spec.ts | 37 +++++++++++++++---- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts index 607d485..5ffca07 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts +++ b/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts @@ -116,4 +116,12 @@ export class MatchRequestDto { @Min(0) @Max(1) maxDetourDurationRatio?: number; + + @IsOptional() + @IsInt() + page?: number; + + @IsOptional() + @IsInt() + perPage?: number; } diff --git a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts index 7fdee64..47cabfb 100644 --- a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts @@ -25,7 +25,7 @@ export class MatchGrpcController { data: matches, page: 1, perPage: 5, - total: 1, + total: matches.length, }; } catch (e) { throw new RpcException({ diff --git a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts index 7c1ef68..894bda6 100644 --- a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts @@ -1,8 +1,10 @@ +import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { Frequency } from '@modules/matcher/core/domain/match.types'; import { MatchRequestDto } from '@modules/matcher/interface/grpc-controllers/dtos/match.request.dto'; import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; import { MatchGrpcController } from '@modules/matcher/interface/grpc-controllers/match.grpc-controller'; import { QueryBus } from '@nestjs/cqrs'; +import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; const originWaypoint: WaypointDto = { @@ -38,14 +40,19 @@ const punctualMatchRequestDto: MatchRequestDto = { }; const mockQueryBus = { - execute: jest.fn().mockImplementation(() => [ - { - adId: 1, - }, - { - adId: 2, - }, - ]), + execute: jest + .fn() + .mockImplementationOnce(() => [ + { + adId: 1, + }, + { + adId: 2, + }, + ]) + .mockImplementationOnce(() => { + throw new Error(); + }), }; describe('Match Grpc Controller', () => { @@ -74,9 +81,23 @@ describe('Match Grpc Controller', () => { }); it('should return matches', async () => { + jest.spyOn(mockQueryBus, 'execute'); const matchPaginatedResponseDto = await matchGrpcController.match( punctualMatchRequestDto, ); expect(matchPaginatedResponseDto.data).toHaveLength(2); + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw a generic RpcException', async () => { + jest.spyOn(mockQueryBus, 'execute'); + expect.assertions(3); + try { + await matchGrpcController.match(punctualMatchRequestDto); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); + } + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); }); }); From f15e7d11b10978739c607c3554131018da04c9f0 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 25 Aug 2023 15:16:33 +0200 Subject: [PATCH 03/52] use strict null checks --- package-lock.json | 8 +- package.json | 2 +- .../commands/create-ad/create-ad.command.ts | 2 +- .../ad/infrastructure/ad.repository.ts | 8 +- .../application/types/default-params.type.ts | 4 +- .../geography/core/domain/route.entity.ts | 73 +++---------------- .../geography/core/domain/route.types.ts | 2 +- .../geography/infrastructure/geodesic.ts | 1 + .../infrastructure/graphhopper-georouter.ts | 34 +++++---- src/modules/geography/route.mapper.ts | 30 +++----- .../unit/infrastructure/geodesic.spec.ts | 4 +- .../geography/tests/unit/route.mapper.spec.ts | 8 -- .../has-valid-position-indexes.validator.ts | 16 ++-- .../grpc-controllers/dtos/waypoint.dto.ts | 5 +- ...s-valid-position-indexes.validator.spec.ts | 57 ++++++--------- tsconfig.json | 2 +- 16 files changed, 91 insertions(+), 165 deletions(-) diff --git a/package-lock.json b/package-lock.json index 27bd203..32000a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", "@mobicoop/configuration-module": "^1.2.0", - "@mobicoop/ddd-library": "^1.1.0", + "@mobicoop/ddd-library": "file:../../packages/dddlibrary", "@mobicoop/health-module": "^2.0.0", "@mobicoop/message-broker-module": "^1.2.0", "@nestjs/axios": "^2.0.0", @@ -1505,9 +1505,9 @@ } }, "node_modules/@mobicoop/ddd-library": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.1.0.tgz", - "integrity": "sha512-x4X7j2CJYZQPDZgLuZP5TFk59fle1wTPdX++Z2YyD7VwwV+yOmVvMIRfTyLRFUTzLObrd6FKs8mh+g59n9jUlA==", + "version": "1.1.1", + "resolved": "file:../../packages/dddlibrary", + "license": "AGPL", "dependencies": { "@nestjs/event-emitter": "^1.4.2", "@nestjs/microservices": "^9.4.0", diff --git a/package.json b/package.json index ed61f84..ab45121 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", "@mobicoop/configuration-module": "^1.2.0", - "@mobicoop/ddd-library": "^1.1.0", + "@mobicoop/ddd-library": "file:../../packages/dddlibrary", "@mobicoop/health-module": "^2.0.0", "@mobicoop/message-broker-module": "^1.2.0", "@nestjs/axios": "^2.0.0", diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts index 3b1e695..5d9839f 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts @@ -18,7 +18,7 @@ export class CreateAdCommand extends Command { constructor(props: CommandProps) { super(props); - this.id = props.id; + this.id = props.id as string; this.driver = props.driver; this.passenger = props.passenger; this.frequency = props.frequency; diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index ae07fd7..cd10089 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -18,10 +18,10 @@ export type AdBaseModel = { seatsProposed: number; seatsRequested: number; strict: boolean; - driverDuration: number; - driverDistance: number; - passengerDuration: number; - passengerDistance: number; + driverDuration?: number; + driverDistance?: number; + passengerDuration?: number; + passengerDistance?: number; fwdAzimuth: number; backAzimuth: number; createdAt: Date; diff --git a/src/modules/geography/core/application/types/default-params.type.ts b/src/modules/geography/core/application/types/default-params.type.ts index 12ea88e..ba61d39 100644 --- a/src/modules/geography/core/application/types/default-params.type.ts +++ b/src/modules/geography/core/application/types/default-params.type.ts @@ -1,4 +1,4 @@ export type DefaultParams = { - GEOROUTER_TYPE: string; - GEOROUTER_URL: string; + GEOROUTER_TYPE?: string; + GEOROUTER_URL?: string; }; diff --git a/src/modules/geography/core/domain/route.entity.ts b/src/modules/geography/core/domain/route.entity.ts index 46177b7..2930482 100644 --- a/src/modules/geography/core/domain/route.entity.ts +++ b/src/modules/geography/core/domain/route.entity.ts @@ -25,8 +25,9 @@ export class RouteEntity extends AggregateRoot { } catch (e: any) { throw e; } - let driverRoute: Route; - let passengerRoute: Route; + let baseRoute: Route; + let driverRoute: Route | undefined; + let passengerRoute: Route | undefined; if (routes.some((route: Route) => route.type == PathType.GENERIC)) { driverRoute = passengerRoute = routes.find( (route: Route) => route.type == PathType.GENERIC, @@ -41,22 +42,21 @@ export class RouteEntity extends AggregateRoot { ? routes.find((route: Route) => route.type == PathType.PASSENGER) : undefined; } + if (driverRoute) { + baseRoute = driverRoute; + } else { + baseRoute = passengerRoute as Route; + } const routeProps: RouteProps = { driverDistance: driverRoute?.distance, driverDuration: driverRoute?.duration, passengerDistance: passengerRoute?.distance, passengerDuration: passengerRoute?.duration, - fwdAzimuth: driverRoute - ? driverRoute.fwdAzimuth - : passengerRoute.fwdAzimuth, - backAzimuth: driverRoute - ? driverRoute.backAzimuth - : passengerRoute.backAzimuth, - distanceAzimuth: driverRoute - ? driverRoute.distanceAzimuth - : passengerRoute.distanceAzimuth, + fwdAzimuth: baseRoute.fwdAzimuth, + backAzimuth: baseRoute.backAzimuth, + distanceAzimuth: baseRoute.distanceAzimuth, waypoints: create.waypoints, - points: driverRoute ? driverRoute.points : passengerRoute.points, + points: baseRoute.points, }; return new RouteEntity({ id: v4(), @@ -111,52 +111,3 @@ export class RouteEntity extends AggregateRoot { points, }); } - -// import { IGeodesic } from '../interfaces/geodesic.interface'; -// import { Point } from '../types/point.type'; -// import { SpacetimePoint } from './spacetime-point'; - -// export class Route { -// distance: number; -// duration: number; -// fwdAzimuth: number; -// backAzimuth: number; -// distanceAzimuth: number; -// points: Point[]; -// spacetimePoints: SpacetimePoint[]; -// private geodesic: IGeodesic; - -// constructor(geodesic: IGeodesic) { -// this.distance = undefined; -// this.duration = undefined; -// this.fwdAzimuth = undefined; -// this.backAzimuth = undefined; -// this.distanceAzimuth = undefined; -// this.points = []; -// this.spacetimePoints = []; -// this.geodesic = geodesic; -// } - -// setPoints = (points: Point[]): void => { -// this.points = points; -// this.setAzimuth(points); -// }; - -// setSpacetimePoints = (spacetimePoints: SpacetimePoint[]): void => { -// this.spacetimePoints = spacetimePoints; -// }; - -// protected setAzimuth = (points: Point[]): void => { -// const inverse = this.geodesic.inverse( -// points[0].lon, -// points[0].lat, -// points[points.length - 1].lon, -// points[points.length - 1].lat, -// ); -// this.fwdAzimuth = -// inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth); -// this.backAzimuth = -// this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180; -// this.distanceAzimuth = inverse.distance; -// }; -// } diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index 5860261..748c759 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -52,7 +52,7 @@ export type Waypoint = Coordinates & { export type SpacetimePoint = Coordinates & { duration: number; - distance: number; + distance?: number; }; export enum Role { diff --git a/src/modules/geography/infrastructure/geodesic.ts b/src/modules/geography/infrastructure/geodesic.ts index a0f1e76..f7ac3f1 100644 --- a/src/modules/geography/infrastructure/geodesic.ts +++ b/src/modules/geography/infrastructure/geodesic.ts @@ -22,6 +22,7 @@ export class Geodesic implements GeodesicPort { lat2, lon2, ); + if (!azimuth || !distance) throw new Error('Azimuth not found'); return { azimuth, distance }; }; } diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts index ce67939..198fcf3 100644 --- a/src/modules/geography/infrastructure/graphhopper-georouter.ts +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -72,11 +72,12 @@ export class GraphhopperGeorouter implements GeorouterPort { .map((point) => [point.lat, point.lon].join('%2C')) .join('&point='), ].join(''); - const route = await lastValueFrom( + return await lastValueFrom( this.httpService.get(url).pipe( - map((res) => - res.data ? this.createRoute(res, path.type) : undefined, - ), + map((response) => { + if (response.data) return this.createRoute(response, path.type); + throw new Error(); + }), catchError((error: AxiosError) => { if (error.code == AxiosError.ERR_BAD_REQUEST) { throw new RouteNotFoundException( @@ -88,7 +89,6 @@ export class GraphhopperGeorouter implements GeorouterPort { }), ), ); - return route; }), ); return routes; @@ -156,12 +156,20 @@ export class GraphhopperGeorouter implements GeorouterPort { const indices = this.getIndices(points, snappedWaypoints); const times = this.getTimes(durations, indices); const distances = this.getDistances(instructions, indices); - return indices.map((index) => ({ - lon: points[index][1], - lat: points[index][0], - distance: distances.find((distance) => distance.index == index)?.distance, - duration: times.find((time) => time.index == index)?.duration, - })); + return indices.map((index) => { + const duration = times.find((time) => time.index == index); + if (!duration) + throw new Error(`Duration not found for waypoint #${index}`); + const distance = distances.find((distance) => distance.index == index); + if (!distance && instructions.length > 0) + throw new Error(`Distance not found for waypoint #${index}`); + return { + lon: points[index][1], + lat: points[index][0], + distance: distance?.distance, + duration: duration.duration, + }; + }); }; private getIndices = ( @@ -182,7 +190,7 @@ export class GraphhopperGeorouter implements GeorouterPort { index: number; originIndex: number; waypoint: number[]; - nearest: number; + nearest?: number; distance: number; } >{ @@ -209,7 +217,7 @@ export class GraphhopperGeorouter implements GeorouterPort { } } for (const missedWaypoint of missedWaypoints) { - indices[missedWaypoint.originIndex] = missedWaypoint.nearest; + indices[missedWaypoint.originIndex] = missedWaypoint.nearest as number; } return indices; }; diff --git a/src/modules/geography/route.mapper.ts b/src/modules/geography/route.mapper.ts index 4f5d63c..a714ff5 100644 --- a/src/modules/geography/route.mapper.ts +++ b/src/modules/geography/route.mapper.ts @@ -14,26 +14,20 @@ import { RouteResponseDto } from './interface/dtos/route.response.dto'; export class RouteMapper implements Mapper { - toPersistence = (): undefined => { - return undefined; - }; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - toDomain = (): undefined => { - return undefined; - }; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars toResponse = (entity: RouteEntity): RouteResponseDto => { const response = new RouteResponseDto(); - response.driverDistance = Math.round(entity.getProps().driverDistance); - response.driverDuration = Math.round(entity.getProps().driverDuration); - response.passengerDistance = Math.round( - entity.getProps().passengerDistance, - ); - response.passengerDuration = Math.round( - entity.getProps().passengerDuration, - ); + response.driverDistance = entity.getProps().driverDistance + ? Math.round(entity.getProps().driverDistance as number) + : undefined; + response.driverDuration = entity.getProps().driverDuration + ? Math.round(entity.getProps().driverDuration as number) + : undefined; + response.passengerDistance = entity.getProps().passengerDistance + ? Math.round(entity.getProps().passengerDistance as number) + : undefined; + response.passengerDuration = entity.getProps().passengerDuration + ? Math.round(entity.getProps().passengerDuration as number) + : undefined; response.fwdAzimuth = Math.round(entity.getProps().fwdAzimuth); response.backAzimuth = Math.round(entity.getProps().backAzimuth); response.distanceAzimuth = Math.round(entity.getProps().distanceAzimuth); diff --git a/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts b/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts index a71df2e..c094a0e 100644 --- a/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts @@ -8,7 +8,7 @@ describe('Matcher geodesic', () => { 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); + expect(Math.round(inv.azimuth as number)).toBe(45); + expect(Math.round(inv.distance as number)).toBe(156900); }); }); diff --git a/src/modules/geography/tests/unit/route.mapper.spec.ts b/src/modules/geography/tests/unit/route.mapper.spec.ts index 0846a7b..7492a85 100644 --- a/src/modules/geography/tests/unit/route.mapper.spec.ts +++ b/src/modules/geography/tests/unit/route.mapper.spec.ts @@ -16,14 +16,6 @@ describe('Route Mapper', () => { expect(routeMapper).toBeDefined(); }); - it('should map domain entity to persistence data', async () => { - expect(routeMapper.toPersistence()).toBeUndefined(); - }); - - it('should map persisted data to domain entity', async () => { - expect(routeMapper.toDomain()).toBeUndefined(); - }); - it('should map domain entity to response', async () => { const now = new Date(); const routeEntity: RouteEntity = new RouteEntity({ diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts b/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts index 04504a8..302de1c 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts +++ b/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts @@ -1,17 +1,11 @@ import { WaypointDto } from '../waypoint.dto'; export const hasValidPositionIndexes = (waypoints: WaypointDto[]): boolean => { - if (!waypoints) return false; if (waypoints.length == 0) return false; - if (waypoints.every((waypoint) => waypoint.position === undefined)) - return false; - if (waypoints.every((waypoint) => typeof waypoint.position === 'number')) { - const positions = Array.from(waypoints, (waypoint) => waypoint.position); - positions.sort(); - for (let i = 1; i < positions.length; i++) - if (positions[i] != positions[i - 1] + 1) return false; + const positions = Array.from(waypoints, (waypoint) => waypoint.position); + positions.sort(); + for (let i = 1; i < positions.length; i++) + if (positions[i] != positions[i - 1] + 1) return false; - return true; - } - return false; + return true; }; diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts b/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts index ded5386..1d6ebd6 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts +++ b/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts @@ -1,8 +1,7 @@ -import { IsInt, IsOptional } from 'class-validator'; +import { IsInt } from 'class-validator'; import { AddressDto } from './address.dto'; export class WaypointDto extends AddressDto { - @IsOptional() @IsInt() - position?: number; + position: number; } diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts index a0c0661..aed3a13 100644 --- a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts +++ b/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts @@ -1,8 +1,9 @@ import { hasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator'; import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; -describe('addresses position validator', () => { +describe('Waypoint position validator', () => { const mockAddress1: WaypointDto = { + position: 0, lon: 48.689445, lat: 6.17651, houseNumber: '5', @@ -12,6 +13,7 @@ describe('addresses position validator', () => { country: 'France', }; const mockAddress2: WaypointDto = { + position: 1, lon: 48.8566, lat: 2.3522, locality: 'Paris', @@ -19,45 +21,15 @@ describe('addresses position validator', () => { country: 'France', }; const mockAddress3: WaypointDto = { + position: 2, lon: 49.2628, lat: 4.0347, locality: 'Reims', - postalCode: '51454', + postalCode: '51000', country: 'France', }; - it('should not validate if no position is defined', () => { - expect( - hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), - ).toBeFalsy(); - }); - it('should not validate if only one position is defined', () => { - mockAddress1.position = 0; - expect( - hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), - ).toBeFalsy(); - }); - it('should not validate if positions are partially defined', () => { - mockAddress1.position = 0; - mockAddress2.position = null; - mockAddress3.position = undefined; - expect( - hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), - ).toBeFalsy(); - }); - - it('should not validate if multiple positions have same value', () => { - mockAddress1.position = 0; - mockAddress2.position = 1; - mockAddress3.position = 1; - expect( - hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), - ).toBeFalsy(); - }); - it('should validate if all positions are ordered', () => { - mockAddress1.position = 0; - mockAddress2.position = 1; - mockAddress3.position = 2; + it('should validate if positions are ordered', () => { expect( hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), ).toBeTruthy(); @@ -68,8 +40,23 @@ describe('addresses position validator', () => { hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), ).toBeTruthy(); }); + it('should not validate if positions are not valid', () => { + mockAddress1.position = 0; + mockAddress2.position = 2; + mockAddress3.position = 3; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should not validate if multiple positions have same value', () => { + mockAddress1.position = 0; + mockAddress2.position = 1; + mockAddress3.position = 1; + expect( + hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); it('should not validate if no waypoints are defined', () => { - expect(hasValidPositionIndexes(undefined)).toBeFalsy(); expect(hasValidPositionIndexes([])).toBeFalsy(); }); }); diff --git a/tsconfig.json b/tsconfig.json index ed12947..e077152 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "strictNullChecks": false, + "strictNullChecks": true, "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, From a4c63c4233c41a427e24eb397ed2514e02863829 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 25 Aug 2023 16:01:19 +0200 Subject: [PATCH 04/52] fix base files for strict ts --- src/app.module.ts | 14 +++++++++----- src/main.ts | 2 +- src/modules/ad/ad.mapper.ts | 8 +------- src/modules/ad/infrastructure/ad.repository.ts | 3 ++- src/modules/ad/tests/unit/ad.mapper.spec.ts | 4 ---- src/modules/messager/messager.module.ts | 4 ++-- tsconfig.json | 2 +- 7 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 4fcc186..196ba9f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -26,15 +26,19 @@ import { GeographyModule } from '@modules/geography/geography.module'; useFactory: async ( configService: ConfigService, ): Promise => ({ - domain: configService.get('SERVICE_CONFIGURATION_DOMAIN'), + domain: configService.get( + 'SERVICE_CONFIGURATION_DOMAIN', + ) as string, messageBroker: { - uri: configService.get('MESSAGE_BROKER_URI'), - exchange: configService.get('MESSAGE_BROKER_EXCHANGE'), + uri: configService.get('MESSAGE_BROKER_URI') as string, + exchange: configService.get( + 'MESSAGE_BROKER_EXCHANGE', + ) as string, }, redis: { - host: configService.get('REDIS_HOST'), + host: configService.get('REDIS_HOST') as string, password: configService.get('REDIS_PASSWORD'), - port: configService.get('REDIS_PORT'), + port: configService.get('REDIS_PORT') as number, }, setConfigurationBrokerQueue: 'matcher-configuration-create-update', deleteConfigurationQueue: 'matcher-configuration-delete', diff --git a/src/main.ts b/src/main.ts index 8db905d..2db4892 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,6 @@ async function bootstrap() { }); await app.startAllMicroservices(); - await app.listen(process.env.HEALTH_SERVICE_PORT); + await app.listen(process.env.HEALTH_SERVICE_PORT as string); } bootstrap(); diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 81a94c1..75df1d5 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -6,7 +6,6 @@ import { ScheduleItemModel, AdUnsupportedWriteModel, } from './infrastructure/ad.repository'; -import { Frequency } from './core/domain/ad.types'; import { v4 } from 'uuid'; import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; @@ -85,7 +84,7 @@ export class AdMapper props: { driver: record.driver, passenger: record.passenger, - frequency: Frequency[record.frequency], + frequency: record.frequency, fromDate: record.fromDate.toISOString().split('T')[0], toDate: record.toDate.toISOString().split('T')[0], schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({ @@ -120,11 +119,6 @@ export class AdMapper return entity; }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - toResponse = (entity: AdEntity): undefined => { - return undefined; - }; - toUnsupportedPersistence = (entity: AdEntity): AdUnsupportedWriteModel => ({ waypoints: this.directionEncoder.encode(entity.getProps().waypoints), direction: this.directionEncoder.encode(entity.getProps().points), diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index cd10089..56eb5a6 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -7,12 +7,13 @@ import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; import { AdEntity } from '../core/domain/ad.entity'; import { AdMapper } from '../ad.mapper'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; +import { Frequency } from '../core/domain/ad.types'; export type AdBaseModel = { uuid: string; driver: boolean; passenger: boolean; - frequency: string; + frequency: Frequency; fromDate: Date; toDate: Date; seatsProposed: number; diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index ae0bb6d..925961a 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -165,8 +165,4 @@ describe('Ad Mapper', () => { expect(mapped.getProps().schedule[0].time).toBe('07:05'); expect(mapped.getProps().waypoints.length).toBe(2); }); - - it('should map domain entity to response', async () => { - expect(adMapper.toResponse(adEntity)).toBeUndefined(); - }); }); diff --git a/src/modules/messager/messager.module.ts b/src/modules/messager/messager.module.ts index 64bafed..9bb5108 100644 --- a/src/modules/messager/messager.module.ts +++ b/src/modules/messager/messager.module.ts @@ -14,8 +14,8 @@ const imports = [ useFactory: async ( configService: ConfigService, ): Promise => ({ - uri: configService.get('MESSAGE_BROKER_URI'), - exchange: configService.get('MESSAGE_BROKER_EXCHANGE'), + uri: configService.get('MESSAGE_BROKER_URI') as string, + exchange: configService.get('MESSAGE_BROKER_EXCHANGE') as string, name: 'matcher', handlers: { adCreated: { diff --git a/tsconfig.json b/tsconfig.json index e077152..3f2a1e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "incremental": true, "skipLibCheck": true, "strictNullChecks": true, - "noImplicitAny": false, + "noImplicitAny": true, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, From 336ffe2cf59b6ce902565bd328b92e406c851957 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 29 Aug 2023 17:28:38 +0200 Subject: [PATCH 05/52] move matching services to ad module WIP --- .env.dist | 29 +- src/main.ts | 9 +- src/modules/ad/ad.di-tokens.ts | 4 + src/modules/ad/ad.module.ts | 40 ++- .../application/ports/ad.repository.port.ts | 21 +- .../ports/datetime-transformer.port.ts | 26 ++ .../ports/default-params-provider.port.ts | 5 + .../application/ports/default-params.type.ts | 9 + .../application/ports/time-converter.port.ts | 18 + .../application/ports/timezone-finder.port.ts | 3 + .../queries/match/algorithm.abstract.ts | 18 + .../match/completer/completer.abstract.ts | 8 + .../passenger-oriented-waypoints.completer.ts | 7 + .../queries/match/filter/filter.abstract.ts | 8 + .../filter/passenger-oriented-geo.filter.ts | 6 + .../queries/match/match.query-handler.ts | 25 ++ .../application/queries/match/match.query.ts | 17 +- .../match/passenger-oriented-algorithm.ts | 64 ++++ .../core/application/types/address.type.ts} | 4 +- .../core/application/types/algorithm.types.ts | 18 + .../application/types/schedule-item.type.ts | 4 +- .../core/application/types/waypoint.type.ts | 4 +- src/modules/ad/core/domain/match.entity.ts | 17 + src/modules/ad/core/domain/match.types.ts | 9 + .../schedule-item.value-object.ts | 6 +- .../ad/infrastructure/ad.repository.ts | 45 ++- src/modules/ad/infrastructure/ad.selector.ts | 23 ++ .../infrastructure/default-params-provider.ts | 33 ++ .../input-datetime-transformer.ts | 132 ++++++++ .../ad/infrastructure/time-converter.ts | 57 ++++ .../ad/infrastructure/timezone-finder.ts | 16 + .../dtos/match.paginated.response.dto.ts | 0 .../interface/dtos/match.response.dto.ts | 0 .../grpc-controllers/dtos/address.dto.ts | 4 +- .../grpc-controllers/dtos/coordinates.dto.ts | 0 .../dtos/match.request.dto.ts | 95 +++--- .../dtos/schedule-item.dto.ts | 2 +- .../decorators/has-day.decorator.ts | 0 .../has-valid-position-indexes.decorator.ts | 0 .../decorators/is-after-or-equal.decorator.ts | 0 .../has-valid-position-indexes.validator.ts | 0 .../grpc-controllers/dtos/waypoint.dto.ts | 0 .../grpc-controllers/match.grpc-controller.ts | 2 +- .../interface/grpc-controllers/matcher.proto | 63 ++++ .../ad/tests/unit/core/match.entity.spec.ts | 10 + .../unit/core/match.query-handler.spec.ts | 75 +++++ .../core/passenger-oriented-algorithm.spec.ts | 65 ++++ .../default-param.provider.spec.ts | 58 ++++ .../input-datetime-transformer.spec.ts | 271 +++++++++++++++ .../infrastructure/time-converter.spec.ts | 309 ++++++++++++++++++ .../infrastructure/timezone-finder.spec.ts | 14 + .../unit/interface/has-day.decorator.spec.ts | 4 +- ...s-valid-position-indexes.decorator.spec.ts | 4 +- ...s-valid-position-indexes.validator.spec.ts | 4 +- .../is-after-or-equal.decorator.spec.ts | 2 +- .../interface/match.grpc.controller.spec.ts | 14 +- .../core/application/types/coordinates.ts | 4 - .../core/application/types/schedule-item.ts | 5 - .../core/application/types/waypoint.ts | 5 - .../matcher/core/domain/match.types.ts | 8 - 60 files changed, 1584 insertions(+), 119 deletions(-) create mode 100644 src/modules/ad/core/application/ports/datetime-transformer.port.ts create mode 100644 src/modules/ad/core/application/ports/default-params-provider.port.ts create mode 100644 src/modules/ad/core/application/ports/default-params.type.ts create mode 100644 src/modules/ad/core/application/ports/time-converter.port.ts create mode 100644 src/modules/ad/core/application/ports/timezone-finder.port.ts create mode 100644 src/modules/ad/core/application/queries/match/algorithm.abstract.ts create mode 100644 src/modules/ad/core/application/queries/match/completer/completer.abstract.ts create mode 100644 src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts create mode 100644 src/modules/ad/core/application/queries/match/filter/filter.abstract.ts create mode 100644 src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts create mode 100644 src/modules/ad/core/application/queries/match/match.query-handler.ts rename src/modules/{matcher => ad}/core/application/queries/match/match.query.ts (55%) create mode 100644 src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts rename src/modules/{matcher/core/application/types/address.ts => ad/core/application/types/address.type.ts} (67%) create mode 100644 src/modules/ad/core/application/types/algorithm.types.ts create mode 100644 src/modules/ad/core/domain/match.entity.ts create mode 100644 src/modules/ad/core/domain/match.types.ts create mode 100644 src/modules/ad/infrastructure/ad.selector.ts create mode 100644 src/modules/ad/infrastructure/default-params-provider.ts create mode 100644 src/modules/ad/infrastructure/input-datetime-transformer.ts create mode 100644 src/modules/ad/infrastructure/time-converter.ts create mode 100644 src/modules/ad/infrastructure/timezone-finder.ts rename src/modules/{matcher => ad}/interface/dtos/match.paginated.response.dto.ts (100%) rename src/modules/{matcher => ad}/interface/dtos/match.response.dto.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/address.dto.ts (89%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/coordinates.dto.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/match.request.dto.ts (58%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/schedule-item.dto.ts (75%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/waypoint.dto.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/match.grpc-controller.ts (92%) create mode 100644 src/modules/ad/interface/grpc-controllers/matcher.proto create mode 100644 src/modules/ad/tests/unit/core/match.entity.spec.ts create mode 100644 src/modules/ad/tests/unit/core/match.query-handler.spec.ts create mode 100644 src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts rename src/modules/{matcher => ad}/tests/unit/interface/has-day.decorator.spec.ts (89%) rename src/modules/{matcher => ad}/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts (87%) rename src/modules/{matcher => ad}/tests/unit/interface/has-valid-position-indexes.validator.spec.ts (87%) rename src/modules/{matcher => ad}/tests/unit/interface/is-after-or-equal.decorator.spec.ts (92%) rename src/modules/{matcher => ad}/tests/unit/interface/match.grpc.controller.spec.ts (81%) delete mode 100644 src/modules/matcher/core/application/types/coordinates.ts delete mode 100644 src/modules/matcher/core/application/types/schedule-item.ts delete mode 100644 src/modules/matcher/core/application/types/waypoint.ts delete mode 100644 src/modules/matcher/core/domain/match.types.ts diff --git a/.env.dist b/.env.dist index fa384ef..8f1ee3f 100644 --- a/.env.dist +++ b/.env.dist @@ -23,24 +23,17 @@ CACHE_TTL=5000 # default identifier used for match requests DEFAULT_UUID=00000000-0000-0000-0000-000000000000 -# default number of seats proposed as driver -DEFAULT_SEATS=3 # algorithm type -ALGORITHM=CLASSIC -# strict algorithm (if relevant with the algorithm type) -# if set to true, matches are made so that -# punctual ads match only with punctual ads and -# recurrent ads match only with recurrent ads -STRICT_ALGORITHM=0 +ALGORITHM=PASSENGER_ORIENTED # max distance in metres between driver # route and passenger pick-up / drop-off REMOTENESS=15000 # use passenger proportion -USE_PROPORTION=1 +USE_PROPORTION=true # minimal driver proportion PROPORTION=0.3 # use azimuth calculation -USE_AZIMUTH=1 +USE_AZIMUTH=true # azimuth margin AZIMUTH_MARGIN=10 # margin duration in seconds @@ -54,3 +47,19 @@ MAX_DETOUR_DURATION_RATIO=0.3 GEOROUTER_TYPE=graphhopper # georouter url GEOROUTER_URL=http://localhost:8989 + +# DEFAULT CARPOOL DEPARTURE TIME MARGIN (in seconds) +DEPARTURE_TIME_MARGIN=900 + +# DEFAULT ROLE +ROLE=passenger + +# SEATS PROPOSED AS DRIVER / REQUESTED AS PASSENGER +SEATS_PROPOSED=3 +SEATS_REQUESTED=1 + +# ACCEPT ONLY SAME FREQUENCY REQUESTS +STRICT_FREQUENCY=false + +# default timezone +DEFAULT_TIMEZONE=Europe/Paris diff --git a/src/main.ts b/src/main.ts index 2db4892..2c26622 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,10 +11,13 @@ async function bootstrap() { app.connectMicroservice({ transport: Transport.GRPC, options: { - package: ['health'], - protoPath: [join(__dirname, 'health.proto')], + package: ['matcher', 'health'], + protoPath: [ + join(__dirname, 'modules/ad/interface/grpc-controllers/matcher.proto'), + join(__dirname, 'health.proto'), + ], url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, - loader: { keepCase: true }, + loader: { keepCase: true, enums: String }, }, }); diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 8690c8d..4a69ae2 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -5,3 +5,7 @@ export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol( 'AD_GET_BASIC_ROUTE_CONTROLLER', ); export const AD_ROUTE_PROVIDER = Symbol('AD_ROUTE_PROVIDER'); +export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); +export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); +export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); +export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER'); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 53788ec..90610bd 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -6,6 +6,10 @@ import { AD_DIRECTION_ENCODER, AD_ROUTE_PROVIDER, AD_GET_BASIC_ROUTE_CONTROLLER, + PARAMS_PROVIDER, + TIMEZONE_FINDER, + TIME_CONVERTER, + INPUT_DATETIME_TRANSFORMER, } from './ad.di-tokens'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { AdRepository } from './infrastructure/ad.repository'; @@ -17,11 +21,21 @@ import { GetBasicRouteController } from '@modules/geography/interface/controller import { RouteProvider } from './infrastructure/route-provider'; import { GeographyModule } from '@modules/geography/geography.module'; import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; +import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller'; +import { MatchQueryHandler } from './core/application/queries/match/match.query-handler'; +import { DefaultParamsProvider } from './infrastructure/default-params-provider'; +import { TimezoneFinder } from './infrastructure/timezone-finder'; +import { TimeConverter } from './infrastructure/time-converter'; +import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; + +const grpcControllers = [MatchGrpcController]; const messageHandlers = [AdCreatedMessageHandler]; const commandHandlers: Provider[] = [CreateAdService]; +const queryHandlers: Provider[] = [MatchQueryHandler]; + const mappers: Provider[] = [AdMapper]; const repositories: Provider[] = [ @@ -53,19 +67,43 @@ const adapters: Provider[] = [ provide: AD_GET_BASIC_ROUTE_CONTROLLER, useClass: GetBasicRouteController, }, + { + provide: PARAMS_PROVIDER, + useClass: DefaultParamsProvider, + }, + { + provide: TIMEZONE_FINDER, + useClass: TimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useClass: TimeConverter, + }, + { + provide: INPUT_DATETIME_TRANSFORMER, + useClass: InputDateTimeTransformer, + }, ]; @Module({ imports: [CqrsModule, GeographyModule], + controllers: [...grpcControllers], providers: [ ...messageHandlers, ...commandHandlers, + ...queryHandlers, ...mappers, ...repositories, ...messagePublishers, ...orms, ...adapters, ], - exports: [PrismaService, AdMapper, AD_REPOSITORY, AD_DIRECTION_ENCODER], + exports: [ + PrismaService, + AdMapper, + AD_REPOSITORY, + AD_DIRECTION_ENCODER, + AD_MESSAGE_PUBLISHER, + ], }) export class AdModule {} diff --git a/src/modules/ad/core/application/ports/ad.repository.port.ts b/src/modules/ad/core/application/ports/ad.repository.port.ts index 91b5294..af68529 100644 --- a/src/modules/ad/core/application/ports/ad.repository.port.ts +++ b/src/modules/ad/core/application/ports/ad.repository.port.ts @@ -1,4 +1,23 @@ import { ExtendedRepositoryPort } from '@mobicoop/ddd-library'; import { AdEntity } from '../../domain/ad.entity'; +import { AlgorithmType, Candidate } from '../types/algorithm.types'; +import { Frequency } from '../../domain/ad.types'; -export type AdRepositoryPort = ExtendedRepositoryPort; +export type AdRepositoryPort = ExtendedRepositoryPort & { + getCandidates(query: CandidateQuery): Promise; +}; + +export type CandidateQuery = { + driver: boolean; + passenger: boolean; + strict: boolean; + frequency: Frequency; + fromDate: string; + toDate: string; + schedule: { + day?: number; + time: string; + margin?: number; + }[]; + algorithmType: AlgorithmType; +}; diff --git a/src/modules/ad/core/application/ports/datetime-transformer.port.ts b/src/modules/ad/core/application/ports/datetime-transformer.port.ts new file mode 100644 index 0000000..4b651c0 --- /dev/null +++ b/src/modules/ad/core/application/ports/datetime-transformer.port.ts @@ -0,0 +1,26 @@ +export interface DateTimeTransformerPort { + fromDate(geoFromDate: GeoDateTime, frequency: Frequency): string; + toDate( + toDate: string, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): string; + day(day: number, geoFromDate: GeoDateTime, frequency: Frequency): number; + time(geoFromDate: GeoDateTime, frequency: Frequency): string; +} + +export type GeoDateTime = { + date: string; + time: string; + coordinates: Coordinates; +}; + +export type Coordinates = { + lon: number; + lat: number; +}; + +export enum Frequency { + PUNCTUAL = 'PUNCTUAL', + RECURRENT = 'RECURRENT', +} diff --git a/src/modules/ad/core/application/ports/default-params-provider.port.ts b/src/modules/ad/core/application/ports/default-params-provider.port.ts new file mode 100644 index 0000000..e316b77 --- /dev/null +++ b/src/modules/ad/core/application/ports/default-params-provider.port.ts @@ -0,0 +1,5 @@ +import { DefaultParams } from './default-params.type'; + +export interface DefaultParamsProviderPort { + getParams(): DefaultParams; +} diff --git a/src/modules/ad/core/application/ports/default-params.type.ts b/src/modules/ad/core/application/ports/default-params.type.ts new file mode 100644 index 0000000..dbf0798 --- /dev/null +++ b/src/modules/ad/core/application/ports/default-params.type.ts @@ -0,0 +1,9 @@ +export type DefaultParams = { + DRIVER: boolean; + PASSENGER: boolean; + SEATS_PROPOSED: number; + SEATS_REQUESTED: number; + DEPARTURE_TIME_MARGIN: number; + STRICT: boolean; + DEFAULT_TIMEZONE: string; +}; diff --git a/src/modules/ad/core/application/ports/time-converter.port.ts b/src/modules/ad/core/application/ports/time-converter.port.ts new file mode 100644 index 0000000..112340f --- /dev/null +++ b/src/modules/ad/core/application/ports/time-converter.port.ts @@ -0,0 +1,18 @@ +export interface TimeConverterPort { + localStringTimeToUtcStringTime(time: string, timezone: string): string; + utcStringTimeToLocalStringTime(time: string, timezone: string): string; + localStringDateTimeToUtcDate( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): Date; + utcStringDateTimeToLocalIsoString( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): string; + utcUnixEpochDayFromTime(time: string, timezone: string): number; + localUnixEpochDayFromTime(time: string, timezone: string): number; +} diff --git a/src/modules/ad/core/application/ports/timezone-finder.port.ts b/src/modules/ad/core/application/ports/timezone-finder.port.ts new file mode 100644 index 0000000..72ba115 --- /dev/null +++ b/src/modules/ad/core/application/ports/timezone-finder.port.ts @@ -0,0 +1,3 @@ +export interface TimezoneFinderPort { + timezones(lon: number, lat: number, defaultTimezone?: string): string[]; +} diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts new file mode 100644 index 0000000..3bd3f82 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -0,0 +1,18 @@ +import { MatchEntity } from '../../../domain/match.entity'; +import { Candidate, Processor } from '../../types/algorithm.types'; +import { MatchQuery } from './match.query'; +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; + +export abstract class Algorithm { + protected candidates: Candidate[]; + protected processors: Processor[]; + constructor( + protected readonly query: MatchQuery, + protected readonly repository: AdRepositoryPort, + ) {} + + /** + * Filter candidates that matches the query + */ + abstract match(): Promise; +} diff --git a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts new file mode 100644 index 0000000..9034bcc --- /dev/null +++ b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts @@ -0,0 +1,8 @@ +import { Candidate, Processor } from '../../../types/algorithm.types'; + +export abstract class Completer implements Processor { + execute = async (candidates: Candidate[]): Promise => + this.complete(candidates); + + abstract complete(candidates: Candidate[]): Promise; +} diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts new file mode 100644 index 0000000..9f2f4ee --- /dev/null +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts @@ -0,0 +1,7 @@ +import { Candidate } from '../../../types/algorithm.types'; +import { Completer } from './completer.abstract'; + +export class PassengerOrientedWaypointsCompleter extends Completer { + complete = async (candidates: Candidate[]): Promise => + candidates; +} diff --git a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts new file mode 100644 index 0000000..5461f33 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts @@ -0,0 +1,8 @@ +import { Candidate, Processor } from '../../../types/algorithm.types'; + +export abstract class Filter implements Processor { + execute = async (candidates: Candidate[]): Promise => + this.filter(candidates); + + abstract filter(candidates: Candidate[]): Promise; +} diff --git a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts new file mode 100644 index 0000000..2d13980 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts @@ -0,0 +1,6 @@ +import { Candidate } from '../../../types/algorithm.types'; +import { Filter } from './filter.abstract'; + +export class PassengerOrientedGeoFilter extends Filter { + filter = async (candidates: Candidate[]): Promise => candidates; +} diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts new file mode 100644 index 0000000..404402f --- /dev/null +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -0,0 +1,25 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { MatchQuery } from './match.query'; +import { Algorithm } from './algorithm.abstract'; +import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm'; +import { AlgorithmType } from '../../types/algorithm.types'; +import { Inject } from '@nestjs/common'; +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; + +@QueryHandler(MatchQuery) +export class MatchQueryHandler implements IQueryHandler { + constructor( + @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, + ) {} + execute = async (query: MatchQuery): Promise => { + let algorithm: Algorithm; + switch (query.algorithmType) { + case AlgorithmType.PASSENGER_ORIENTED: + default: + algorithm = new PassengerOrientedAlgorithm(query, this.repository); + } + return algorithm.match(); + }; +} diff --git a/src/modules/matcher/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts similarity index 55% rename from src/modules/matcher/core/application/queries/match/match.query.ts rename to src/modules/ad/core/application/queries/match/match.query.ts index 73531c7..3b0d480 100644 --- a/src/modules/matcher/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -1,17 +1,19 @@ import { QueryBase } from '@mobicoop/ddd-library'; -import { Frequency } from '@modules/matcher/core/domain/match.types'; -import { ScheduleItem } from '../../types/schedule-item'; -import { Waypoint } from '../../types/waypoint'; +import { AlgorithmType } from '../../types/algorithm.types'; +import { Waypoint } from '../../types/waypoint.type'; +import { ScheduleItem } from '../../types/schedule-item.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; export class MatchQuery extends QueryBase { - readonly driver?: boolean; - readonly passenger?: boolean; - readonly frequency?: Frequency; + readonly driver: boolean; + readonly passenger: boolean; + readonly frequency: Frequency; readonly fromDate: string; readonly toDate: string; readonly schedule: ScheduleItem[]; - readonly strict?: boolean; + readonly strict: boolean; readonly waypoints: Waypoint[]; + readonly algorithmType: AlgorithmType; constructor(props: MatchQuery) { super(); @@ -23,5 +25,6 @@ export class MatchQuery extends QueryBase { this.schedule = props.schedule; this.strict = props.strict; this.waypoints = props.waypoints; + this.algorithmType = props.algorithmType; } } diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts new file mode 100644 index 0000000..16ac2e7 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -0,0 +1,64 @@ +import { Algorithm } from './algorithm.abstract'; +import { MatchQuery } from './match.query'; +import { PassengerOrientedWaypointsCompleter } from './completer/passenger-oriented-waypoints.completer'; +import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { Candidate } from '../../types/algorithm.types'; + +export class PassengerOrientedAlgorithm extends Algorithm { + constructor( + protected readonly query: MatchQuery, + protected readonly repository: AdRepositoryPort, + ) { + super(query, repository); + this.processors = [ + new PassengerOrientedWaypointsCompleter(), + new PassengerOrientedGeoFilter(), + ]; + this.candidates = [ + { + ad: { + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + }, + role: Role.DRIVER, + }, + ]; + } + // this.candidates = ( + // await Promise.all( + // sqlQueries.map( + // async (queryRole: QueryRole) => + // ({ + // ads: (await this.repository.queryRawUnsafe( + // queryRole.query, + // )) as AdEntity[], + // role: queryRole.role, + // } as AdsRole), + // ), + // ) + // ) + // .map((adsRole: AdsRole) => + // adsRole.ads.map( + // (adEntity: AdEntity) => + // { + // ad: { + // id: adEntity.id, + // }, + // role: adsRole.role, + // }, + // ), + // ) + // .flat(); + + match = async (): Promise => { + this.candidates = await this.repository.getCandidates(this.query); + for (const processor of this.processors) { + this.candidates = await processor.execute(this.candidates); + } + return this.candidates.map((candidate: Candidate) => + MatchEntity.create({ adId: candidate.ad.id }), + ); + }; +} diff --git a/src/modules/matcher/core/application/types/address.ts b/src/modules/ad/core/application/types/address.type.ts similarity index 67% rename from src/modules/matcher/core/application/types/address.ts rename to src/modules/ad/core/application/types/address.type.ts index bdf8e6b..e37174a 100644 --- a/src/modules/matcher/core/application/types/address.ts +++ b/src/modules/ad/core/application/types/address.type.ts @@ -1,4 +1,4 @@ -import { Coordinates } from './coordinates'; +import { Coordinates } from './coordinates.type'; export type Address = { name?: string; @@ -6,5 +6,5 @@ export type Address = { street?: string; locality?: string; postalCode?: string; - country: string; + country?: string; } & Coordinates; diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts new file mode 100644 index 0000000..017f6a4 --- /dev/null +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -0,0 +1,18 @@ +import { Role } from '../../domain/ad.types'; + +export enum AlgorithmType { + PASSENGER_ORIENTED = 'PASSENGER_ORIENTED', +} + +export interface Processor { + execute(candidates: Candidate[]): Promise; +} + +export type Candidate = { + ad: Ad; + role: Role; +}; + +export type Ad = { + id: string; +}; diff --git a/src/modules/ad/core/application/types/schedule-item.type.ts b/src/modules/ad/core/application/types/schedule-item.type.ts index 92dab99..a40e06d 100644 --- a/src/modules/ad/core/application/types/schedule-item.type.ts +++ b/src/modules/ad/core/application/types/schedule-item.type.ts @@ -1,5 +1,5 @@ export type ScheduleItem = { - day: number; + day?: number; time: string; - margin: number; + margin?: number; }; diff --git a/src/modules/ad/core/application/types/waypoint.type.ts b/src/modules/ad/core/application/types/waypoint.type.ts index ba91158..b08efad 100644 --- a/src/modules/ad/core/application/types/waypoint.type.ts +++ b/src/modules/ad/core/application/types/waypoint.type.ts @@ -1,5 +1,5 @@ -import { Coordinates } from './coordinates.type'; +import { Address } from './address.type'; export type Waypoint = { position: number; -} & Coordinates; +} & Address; diff --git a/src/modules/ad/core/domain/match.entity.ts b/src/modules/ad/core/domain/match.entity.ts new file mode 100644 index 0000000..520fe28 --- /dev/null +++ b/src/modules/ad/core/domain/match.entity.ts @@ -0,0 +1,17 @@ +import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; +import { v4 } from 'uuid'; +import { CreateMatchProps, MatchProps } from './match.types'; + +export class MatchEntity extends AggregateRoot { + protected readonly _id: AggregateID; + + static create = (create: CreateMatchProps): MatchEntity => { + const id = v4(); + const props: MatchProps = { ...create }; + return new MatchEntity({ id, props }); + }; + + validate(): void { + // entity business rules validation to protect it's invariant before saving entity to a database + } +} diff --git a/src/modules/ad/core/domain/match.types.ts b/src/modules/ad/core/domain/match.types.ts new file mode 100644 index 0000000..997f87b --- /dev/null +++ b/src/modules/ad/core/domain/match.types.ts @@ -0,0 +1,9 @@ +// All properties that a Match has +export interface MatchProps { + adId: string; +} + +// Properties that are needed for a Match creation +export interface CreateMatchProps { + adId: string; +} diff --git a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts index 5f2d66b..4a773a4 100644 --- a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts @@ -10,9 +10,9 @@ import { * */ export interface ScheduleItemProps { - day: number; + day?: number; time: string; - margin: number; + margin?: number; } export class ScheduleItem extends ValueObject { @@ -30,7 +30,7 @@ export class ScheduleItem extends ValueObject { // eslint-disable-next-line @typescript-eslint/no-unused-vars protected validate(props: ScheduleItemProps): void { - if (props.day < 0 || props.day > 6) + if (props.day !== undefined && (props.day < 0 || props.day > 6)) throw new ArgumentOutOfRangeException('day must be between 0 and 6'); if (props.time.split(':').length != 2) throw new ArgumentInvalidException('time is invalid'); diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 56eb5a6..1a33b8a 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -1,13 +1,18 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { AdRepositoryPort } from '../core/application/ports/ad.repository.port'; +import { + AdRepositoryPort, + CandidateQuery, +} from '../core/application/ports/ad.repository.port'; import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library'; import { PrismaService } from './prisma.service'; import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; import { AdEntity } from '../core/domain/ad.entity'; import { AdMapper } from '../ad.mapper'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; -import { Frequency } from '../core/domain/ad.types'; +import { Frequency, Role } from '../core/domain/ad.types'; +import { Candidate } from '../core/application/types/algorithm.types'; +import { AdSelector } from './ad.selector'; export type AdBaseModel = { uuid: string; @@ -87,4 +92,40 @@ export class AdRepository }), ); } + + getCandidates = async (query: CandidateQuery): Promise => { + // let candidates: Candidate[] = []; + const sqlQueries: QueryRole[] = []; + if (query.driver) + sqlQueries.push({ + query: AdSelector.select(Role.DRIVER, query), + role: Role.DRIVER, + }); + if (query.passenger) + sqlQueries.push({ + query: AdSelector.select(Role.PASSENGER, query), + role: Role.PASSENGER, + }); + const results = await Promise.all( + sqlQueries.map( + async (queryRole: QueryRole) => + ({ + ads: (await this.queryRawUnsafe(queryRole.query)) as AdEntity[], + role: queryRole.role, + } as AdsRole), + ), + ); + console.log(results[0].ads); + return []; + }; } + +type QueryRole = { + query: string; + role: Role; +}; + +type AdsRole = { + ads: AdEntity[]; + role: Role; +}; diff --git a/src/modules/ad/infrastructure/ad.selector.ts b/src/modules/ad/infrastructure/ad.selector.ts new file mode 100644 index 0000000..74e91db --- /dev/null +++ b/src/modules/ad/infrastructure/ad.selector.ts @@ -0,0 +1,23 @@ +import { CandidateQuery } from '../core/application/ports/ad.repository.port'; +import { AlgorithmType } from '../core/application/types/algorithm.types'; +import { Role } from '../core/domain/ad.types'; + +export class AdSelector { + static select = (role: Role, query: CandidateQuery): string => { + switch (query.algorithmType) { + case AlgorithmType.PASSENGER_ORIENTED: + default: + return `SELECT + ad.uuid,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, + "fromDate","toDate", + "seatsProposed","seatsRequested", + strict, + "driverDuration","driverDistance", + "passengerDuration","passengerDistance", + "fwdAzimuth","backAzimuth", + si.day,si.time,si.margin + FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" + WHERE driver=True`; + } + }; +} diff --git a/src/modules/ad/infrastructure/default-params-provider.ts b/src/modules/ad/infrastructure/default-params-provider.ts new file mode 100644 index 0000000..7244e39 --- /dev/null +++ b/src/modules/ad/infrastructure/default-params-provider.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; +import { DefaultParams } from '../core/application/ports/default-params.type'; + +const DEFAULT_SEATS_PROPOSED = 3; +const DEFAULT_SEATS_REQUESTED = 1; +const DEFAULT_DEPARTURE_TIME_MARGIN = 900; +const DEFAULT_TIMEZONE = 'Europe/Paris'; + +@Injectable() +export class DefaultParamsProvider implements DefaultParamsProviderPort { + constructor(private readonly _configService: ConfigService) {} + getParams = (): DefaultParams => ({ + DRIVER: this._configService.get('ROLE') == 'driver', + SEATS_PROPOSED: + this._configService.get('SEATS_PROPOSED') !== undefined + ? parseInt(this._configService.get('SEATS_PROPOSED') as string) + : DEFAULT_SEATS_PROPOSED, + PASSENGER: this._configService.get('ROLE') == 'passenger', + SEATS_REQUESTED: + this._configService.get('SEATS_REQUESTED') !== undefined + ? parseInt(this._configService.get('SEATS_REQUESTED') as string) + : DEFAULT_SEATS_REQUESTED, + DEPARTURE_TIME_MARGIN: + this._configService.get('DEPARTURE_TIME_MARGIN') !== undefined + ? parseInt(this._configService.get('DEPARTURE_TIME_MARGIN') as string) + : DEFAULT_DEPARTURE_TIME_MARGIN, + STRICT: this._configService.get('STRICT_FREQUENCY') == 'true', + DEFAULT_TIMEZONE: + this._configService.get('DEFAULT_TIMEZONE') ?? DEFAULT_TIMEZONE, + }); +} diff --git a/src/modules/ad/infrastructure/input-datetime-transformer.ts b/src/modules/ad/infrastructure/input-datetime-transformer.ts new file mode 100644 index 0000000..faa4025 --- /dev/null +++ b/src/modules/ad/infrastructure/input-datetime-transformer.ts @@ -0,0 +1,132 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + DateTimeTransformerPort, + Frequency, + GeoDateTime, +} from '../core/application/ports/datetime-transformer.port'; +import { TimeConverterPort } from '../core/application/ports/time-converter.port'; +import { + PARAMS_PROVIDER, + TIMEZONE_FINDER, + TIME_CONVERTER, +} from '../ad.di-tokens'; +import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; +import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; + +@Injectable() +export class InputDateTimeTransformer implements DateTimeTransformerPort { + private readonly _defaultTimezone: string; + constructor( + @Inject(PARAMS_PROVIDER) + private readonly defaultParamsProvider: DefaultParamsProviderPort, + @Inject(TIMEZONE_FINDER) + private readonly timezoneFinder: TimezoneFinderPort, + @Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort, + ) { + this._defaultTimezone = defaultParamsProvider.getParams().DEFAULT_TIMEZONE; + } + + /** + * Compute the fromDate : if an ad is punctual, the departure date + * is converted to UTC with the time and timezone + */ + fromDate = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) return geoFromDate.date; + return this.timeConverter + .localStringDateTimeToUtcDate( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ) + .toISOString() + .split('T')[0]; + }; + + /** + * Get the toDate depending on frequency, time and timezone : + * if the ad is punctual, the toDate is equal to the fromDate + */ + toDate = ( + toDate: string, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): string => { + if (frequency === Frequency.RECURRENT) return toDate; + return this.fromDate(geoFromDate, frequency); + }; + + /** + * Get the day for a schedule item : + * - if the ad is punctual, the day is infered from fromDate + * - if the ad is recurrent, the day is computed by converting the time to utc + */ + day = ( + day: number, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): number => { + if (frequency === Frequency.RECURRENT) + return this.recurrentDay( + day, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ); + return new Date(this.fromDate(geoFromDate, frequency)).getDay(); + }; + + /** + * Get the utc time + */ + time = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) + return this.timeConverter.localStringTimeToUtcStringTime( + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ); + return this.timeConverter + .localStringDateTimeToUtcDate( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ) + .toISOString() + .split('T')[1] + .split(':', 2) + .join(':'); + }; + + /** + * Get the day for a schedule item for a recurrent ad + * The day may change when transforming from local timezone to utc + */ + private recurrentDay = ( + day: number, + time: string, + timezone: string, + ): number => { + const unixEpochDay = 4; // 1970-01-01 is a thursday ! + const utcBaseDay = this.timeConverter.utcUnixEpochDayFromTime( + time, + timezone, + ); + if (unixEpochDay == utcBaseDay) return day; + if (unixEpochDay > utcBaseDay) return day > 0 ? day - 1 : 6; + return day < 6 ? day + 1 : 0; + }; +} diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts new file mode 100644 index 0000000..c3b955f --- /dev/null +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { DateTime, TimeZone } from 'timezonecomplete'; +import { TimeConverterPort } from '../core/application/ports/time-converter.port'; + +@Injectable() +export class TimeConverter implements TimeConverterPort { + private readonly UNIX_EPOCH = '1970-01-01'; + + localStringTimeToUtcStringTime = (time: string, timezone: string): string => + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone(timezone)) + .convert(TimeZone.zone('UTC')) + .format('HH:mm'); + + utcStringTimeToLocalStringTime = (time: string, timezone: string): string => + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone)) + .format('HH:mm'); + + localStringDateTimeToUtcDate = ( + date: string, + time: string, + timezone: string, + dst = true, + ): Date => + new Date( + new DateTime( + `${date}T${time}`, + TimeZone.zone(timezone, dst), + ).toIsoString(), + ); + + utcStringDateTimeToLocalIsoString = ( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): string => + new DateTime(`${date}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone, dst)) + .toIsoString(); + + utcUnixEpochDayFromTime = (time: string, timezone: string): number => + new Date( + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone(timezone, false)) + .convert(TimeZone.zone('UTC')) + .toIsoString() + .split('T')[0], + ).getDay(); + + localUnixEpochDayFromTime = (time: string, timezone: string): number => + new Date( + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone)) + .toIsoString() + .split('T')[0], + ).getDay(); +} diff --git a/src/modules/ad/infrastructure/timezone-finder.ts b/src/modules/ad/infrastructure/timezone-finder.ts new file mode 100644 index 0000000..feb0b5a --- /dev/null +++ b/src/modules/ad/infrastructure/timezone-finder.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { find } from 'geo-tz'; +import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; + +@Injectable() +export class TimezoneFinder implements TimezoneFinderPort { + timezones = ( + lon: number, + lat: number, + defaultTimezone?: string, + ): string[] => { + const foundTimezones = find(lat, lon); + if (defaultTimezone && foundTimezones.length == 0) return [defaultTimezone]; + return foundTimezones; + }; +} diff --git a/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts b/src/modules/ad/interface/dtos/match.paginated.response.dto.ts similarity index 100% rename from src/modules/matcher/interface/dtos/match.paginated.response.dto.ts rename to src/modules/ad/interface/dtos/match.paginated.response.dto.ts diff --git a/src/modules/matcher/interface/dtos/match.response.dto.ts b/src/modules/ad/interface/dtos/match.response.dto.ts similarity index 100% rename from src/modules/matcher/interface/dtos/match.response.dto.ts rename to src/modules/ad/interface/dtos/match.response.dto.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/address.dto.ts similarity index 89% rename from src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/address.dto.ts index 9659d96..d37dfa7 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/address.dto.ts @@ -3,6 +3,7 @@ import { CoordinatesDto as CoordinatesDto } from './coordinates.dto'; export class AddressDto extends CoordinatesDto { @IsOptional() + @IsString() name?: string; @IsOptional() @@ -21,6 +22,7 @@ export class AddressDto extends CoordinatesDto { @IsString() postalCode?: string; + @IsOptional() @IsString() - country: string; + country?: string; } diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts similarity index 58% rename from src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts index 5ffca07..86f9826 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts @@ -1,18 +1,11 @@ -import { - AlgorithmType, - Frequency, -} from '@modules/matcher/core/domain/match.types'; import { ArrayMinSize, IsArray, IsBoolean, - IsDecimal, IsEnum, IsISO8601, IsInt, IsOptional, - Max, - Min, ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; @@ -21,15 +14,17 @@ import { IsAfterOrEqual } from './validators/decorators/is-after-or-equal.decora import { ScheduleItemDto } from './schedule-item.dto'; import { WaypointDto } from './waypoint.dto'; import { HasValidPositionIndexes } from './validators/decorators/has-valid-position-indexes.decorator'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; export class MatchRequestDto { - @IsOptional() + // @IsOptional() @IsBoolean() - driver?: boolean; + driver: boolean; - @IsOptional() + // @IsOptional() @IsBoolean() - passenger?: boolean; + passenger: boolean; @IsEnum(Frequency) @HasDay('schedule', { @@ -58,17 +53,17 @@ export class MatchRequestDto { @ValidateNested({ each: true }) schedule: ScheduleItemDto[]; - @IsOptional() - @IsInt() - seatsProposed?: number; + // @IsOptional() + // @IsInt() + // seatsProposed?: number; - @IsOptional() - @IsInt() - seatsRequested?: number; + // @IsOptional() + // @IsInt() + // seatsRequested?: number; - @IsOptional() + // @IsOptional() @IsBoolean() - strict?: boolean; + strict: boolean; @Type(() => WaypointDto) @IsArray() @@ -77,45 +72,45 @@ export class MatchRequestDto { @ValidateNested({ each: true }) waypoints: WaypointDto[]; - @IsOptional() + // @IsOptional() @IsEnum(AlgorithmType) - algorithm?: AlgorithmType; + algorithmType: AlgorithmType; - @IsOptional() - @IsInt() - remoteness?: number; + // @IsOptional() + // @IsInt() + // remoteness?: number; - @IsOptional() - @IsBoolean() - useProportion?: boolean; + // @IsOptional() + // @IsBoolean() + // useProportion?: boolean; - @IsOptional() - @IsDecimal() - @Min(0) - @Max(1) - proportion?: number; + // @IsOptional() + // @IsDecimal() + // @Min(0) + // @Max(1) + // proportion?: number; - @IsOptional() - @IsBoolean() - useAzimuth?: boolean; + // @IsOptional() + // @IsBoolean() + // useAzimuth?: boolean; - @IsOptional() - @IsInt() - @Min(0) - @Max(359) - azimuthMargin?: number; + // @IsOptional() + // @IsInt() + // @Min(0) + // @Max(359) + // azimuthMargin?: number; - @IsOptional() - @IsDecimal() - @Min(0) - @Max(1) - maxDetourDistanceRatio?: number; + // @IsOptional() + // @IsDecimal() + // @Min(0) + // @Max(1) + // maxDetourDistanceRatio?: number; - @IsOptional() - @IsDecimal() - @Min(0) - @Max(1) - maxDetourDurationRatio?: number; + // @IsOptional() + // @IsDecimal() + // @Min(0) + // @Max(1) + // maxDetourDurationRatio?: number; @IsOptional() @IsInt() diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts similarity index 75% rename from src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts index 112adc2..9c0f734 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsMilitaryTime, IsInt, Min, Max } from 'class-validator'; +import { IsMilitaryTime, IsInt, Min, Max, IsOptional } from 'class-validator'; export class ScheduleItemDto { @IsOptional() diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/waypoint.dto.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/waypoint.dto.ts diff --git a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts similarity index 92% rename from src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts rename to src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index 47cabfb..7d84ec5 100644 --- a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -5,7 +5,7 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; import { QueryBus } from '@nestjs/cqrs'; import { MatchRequestDto } from './dtos/match.request.dto'; -import { MatchQuery } from '@modules/matcher/core/application/queries/match/match.query'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; @UsePipes( new RpcValidationPipe({ diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto new file mode 100644 index 0000000..ceeacd3 --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package matcher; + +service MatcherService { + rpc Match(MatchRequest) returns (Matches); +} + +message MatchRequest { + bool driver = 1; + bool passenger = 2; + Frequency frequency = 3; + string fromDate = 4; + string toDate = 5; + repeated ScheduleItem schedule = 6; + bool strict = 7; + repeated Waypoint waypoints = 8; + AlgorithmType algorithmType = 9; + int32 remoteness = 10; + bool useProportion = 11; + int32 proportion = 12; + bool useAzimuth = 13; + int32 azimuthMargin = 14; + float maxDetourDistanceRatio = 15; + float maxDetourDurationRatio = 16; + int32 identifier = 22; +} + +message ScheduleItem { + int32 day = 1; + string time = 2; + int32 margin = 3; +} + +message Waypoint { + int32 position = 1; + double lon = 2; + double lat = 3; + string name = 4; + string houseNumber = 5; + string street = 6; + string locality = 7; + string postalCode = 8; + string country = 9; +} + +enum Frequency { + PUNCTUAL = 1; + RECURRENT = 2; +} + +enum AlgorithmType { + PASSENGER_ORIENTED = 0; +} + +message Match { + string id = 1; +} + +message Matches { + repeated Match data = 1; + int32 total = 2; +} diff --git a/src/modules/ad/tests/unit/core/match.entity.spec.ts b/src/modules/ad/tests/unit/core/match.entity.spec.ts new file mode 100644 index 0000000..5f2f4e5 --- /dev/null +++ b/src/modules/ad/tests/unit/core/match.entity.spec.ts @@ -0,0 +1,10 @@ +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; + +describe('Match entity create', () => { + it('should create a new match entity', async () => { + const match: MatchEntity = MatchEntity.create({ + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + }); + expect(match.id.length).toBe(36); + }); +}); diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts new file mode 100644 index 0000000..98220ac --- /dev/null +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -0,0 +1,75 @@ +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { Test, TestingModule } from '@nestjs/testing'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const mockAdRepository = { + getCandidates: jest.fn(), +}; + +describe('Match Query Handler', () => { + let matchQueryHandler: MatchQueryHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MatchQueryHandler, + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + ], + }).compile(); + + matchQueryHandler = module.get(MatchQueryHandler); + }); + + it('should be defined', () => { + expect(matchQueryHandler).toBeDefined(); + }); + + it('should return a Match entity', async () => { + const matchQuery = new MatchQuery({ + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + day: 1, + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }); + const matches: MatchEntity[] = await matchQueryHandler.execute(matchQuery); + expect(matches.length).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts new file mode 100644 index 0000000..4aae63c --- /dev/null +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -0,0 +1,65 @@ +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { PassengerOrientedAlgorithm } from '@modules/ad/core/application/queries/match/passenger-oriented-algorithm'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery({ + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], +}); + +const mockMatcherRepository: AdRepositoryPort = { + insertWithUnsupportedFields: jest.fn(), + findOneById: jest.fn(), + findOne: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + updateWhere: jest.fn(), + delete: jest.fn(), + count: jest.fn(), + healthCheck: jest.fn(), + queryRawUnsafe: jest.fn(), + getCandidates: jest.fn(), +}; + +describe('Passenger oriented algorithm', () => { + it('should return matching entities', async () => { + const passengerOrientedAlgorithm: PassengerOrientedAlgorithm = + new PassengerOrientedAlgorithm(matchQuery, mockMatcherRepository); + const matches: MatchEntity[] = await passengerOrientedAlgorithm.match(); + expect(matches.length).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts new file mode 100644 index 0000000..5d017e5 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts @@ -0,0 +1,58 @@ +import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type'; +import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockConfigService = { + get: jest.fn().mockImplementation((value: string) => { + switch (value) { + case 'DEPARTURE_TIME_MARGIN': + return 900; + case 'ROLE': + return 'passenger'; + case 'SEATS_PROPOSED': + return 3; + case 'SEATS_REQUESTED': + return 1; + case 'STRICT_FREQUENCY': + return 'false'; + case 'DEFAULT_TIMEZONE': + return 'Europe/Paris'; + default: + return 'some_default_value'; + } + }), +}; + +describe('DefaultParamsProvider', () => { + let defaultParamsProvider: DefaultParamsProvider; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + DefaultParamsProvider, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + defaultParamsProvider = module.get( + DefaultParamsProvider, + ); + }); + + it('should be defined', () => { + expect(defaultParamsProvider).toBeDefined(); + }); + + it('should provide default params', async () => { + const params: DefaultParams = defaultParamsProvider.getParams(); + expect(params.DEPARTURE_TIME_MARGIN).toBe(900); + expect(params.PASSENGER).toBeTruthy(); + expect(params.DRIVER).toBeFalsy(); + expect(params.DEFAULT_TIMEZONE).toBe('Europe/Paris'); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts new file mode 100644 index 0000000..11733a0 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts @@ -0,0 +1,271 @@ +import { + PARAMS_PROVIDER, + TIMEZONE_FINDER, + TIME_CONVERTER, +} from '@modules/ad/ad.di-tokens'; +import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port'; +import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; +import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port'; +import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port'; +import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockDefaultParamsProvider: DefaultParamsProviderPort = { + getParams: () => { + return { + DEPARTURE_TIME_MARGIN: 900, + DRIVER: false, + SEATS_PROPOSED: 3, + PASSENGER: true, + SEATS_REQUESTED: 1, + STRICT: false, + DEFAULT_TIMEZONE: 'Europe/Paris', + }; + }, +}; + +const mockTimezoneFinder: TimezoneFinderPort = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter: TimeConverterPort = { + localStringTimeToUtcStringTime: jest + .fn() + .mockImplementationOnce(() => '00:15'), + utcStringTimeToLocalStringTime: jest.fn(), + localStringDateTimeToUtcDate: jest + .fn() + .mockImplementationOnce(() => new Date('2023-07-30T06:15:00.000Z')) + .mockImplementationOnce(() => new Date('2023-07-20T08:15:00.000Z')) + .mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')) + .mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')), + utcStringDateTimeToLocalIsoString: jest.fn(), + utcUnixEpochDayFromTime: jest + .fn() + .mockImplementationOnce(() => 4) + .mockImplementationOnce(() => 3) + .mockImplementationOnce(() => 3) + .mockImplementationOnce(() => 5) + .mockImplementationOnce(() => 5), + localUnixEpochDayFromTime: jest.fn(), +}; + +describe('Input Datetime Transformer', () => { + let inputDatetimeTransformer: InputDateTimeTransformer; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: PARAMS_PROVIDER, + useValue: mockDefaultParamsProvider, + }, + { + provide: TIMEZONE_FINDER, + useValue: mockTimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useValue: mockTimeConverter, + }, + InputDateTimeTransformer, + ], + }).compile(); + + inputDatetimeTransformer = module.get( + InputDateTimeTransformer, + ); + }); + + it('should be defined', () => { + expect(inputDatetimeTransformer).toBeDefined(); + }); + + describe('fromDate', () => { + it('should return fromDate as is if frequency is recurrent', () => { + const transformedFromDate: string = inputDatetimeTransformer.fromDate( + { + date: '2023-07-30', + time: '07:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(transformedFromDate).toBe('2023-07-30'); + }); + it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => { + const transformedFromDate: string = inputDatetimeTransformer.fromDate( + { + date: '2023-07-30', + time: '07:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(transformedFromDate).toBe('2023-07-30'); + }); + }); + + describe('toDate', () => { + it('should return toDate as is if frequency is recurrent', () => { + const transformedToDate: string = inputDatetimeTransformer.toDate( + '2024-07-29', + { + date: '2023-07-20', + time: '10:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(transformedToDate).toBe('2024-07-29'); + }); + it('should return transformed fromDate if frequency is punctual', () => { + const transformedToDate: string = inputDatetimeTransformer.toDate( + '2024-07-30', + { + date: '2023-07-20', + time: '10:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(transformedToDate).toBe('2023-07-20'); + }); + }); + + describe('day', () => { + it('should not change day if frequency is recurrent and converted UTC time is on the same day', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '01:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(1); + }); + it('should change day if frequency is recurrent and converted UTC time is on the previous day', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should change day if frequency is recurrent and converted UTC time is on the previous day and given day is sunday', () => { + const day: number = inputDatetimeTransformer.day( + 0, + { + date: '2023-07-23', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(6); + }); + it('should change day if frequency is recurrent and converted UTC time is on the next day', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '23:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(2); + }); + it('should change day if frequency is recurrent and converted UTC time is on the next day and given day is saturday(6)', () => { + const day: number = inputDatetimeTransformer.day( + 6, + { + date: '2023-07-29', + time: '23:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should return utc fromDate day if frequency is punctual', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-20', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(day).toBe(3); + }); + }); + + describe('time', () => { + it('should transform given time to utc time if frequency is recurrent', () => { + const time: string = inputDatetimeTransformer.time( + { + date: '2023-07-24', + time: '01:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(time).toBe('00:15'); + }); + it('should return given time to utc time if frequency is punctual', () => { + const time: string = inputDatetimeTransformer.time( + { + date: '2023-07-24', + time: '01:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(time).toBe('23:15'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts new file mode 100644 index 0000000..df8463a --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts @@ -0,0 +1,309 @@ +import { TimeConverter } from '@modules/ad/infrastructure/time-converter'; + +describe('Time Converter', () => { + it('should be defined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(timeConverter).toBeDefined(); + }); + + describe('localStringTimeToUtcStringTime', () => { + it('should convert a paris time to utc time', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisTime = '08:00'; + const utcDatetime = timeConverter.localStringTimeToUtcStringTime( + parisTime, + 'Europe/Paris', + ); + expect(utcDatetime).toBe('07:00'); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const fooBarTime = '08:00'; + expect(() => { + timeConverter.localStringTimeToUtcStringTime(fooBarTime, 'Foo/Bar'); + }).toThrow(); + }); + }); + + describe('utcStringTimeToLocalStringTime', () => { + it('should convert a utc time to a paris time', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringTimeToLocalStringTime( + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBe('08:00'); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '27:00'; + expect(() => { + timeConverter.utcStringTimeToLocalStringTime(utcTime, 'Europe/Paris'); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '07:00'; + expect(() => { + timeConverter.utcStringTimeToLocalStringTime(utcTime, 'Foo/Bar'); + }).toThrow(); + }); + }); + + describe('localStringDateTimeToUtcDate', () => { + it('should convert a summer paris date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z'); + }); + it('should convert a winter paris date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-02-02'; + const parisTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z'); + }); + it('should convert a summer paris date and time to a utc date without dst', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + false, + ); + expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z'); + }); + it('should convert a tonga date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const tongaDate = '2023-02-02'; + const tongaTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + tongaDate, + tongaTime, + 'Pacific/Tongatapu', + ); + expect(utcDate.toISOString()).toBe('2023-02-01T23:00:00.000Z'); + }); + it('should convert a papeete date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const papeeteDate = '2023-02-02'; + const papeeteTime = '15:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + papeeteDate, + papeeteTime, + 'Pacific/Tahiti', + ); + expect(utcDate.toISOString()).toBe('2023-02-03T01:00:00.000Z'); + }); + it('should throw an error if date is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-32'; + const parisTime = '08:00'; + expect(() => { + timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '28:00'; + expect(() => { + timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '12:00'; + expect(() => { + timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Foo/Bar', + ); + }).toThrow(); + }); + }); + + describe('utcStringDateTimeToLocalIsoString', () => { + it('should convert a utc string date and time to a summer paris date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(localIsoString).toBe('2023-06-22T12:00:00.000+02:00'); + }); + it('should convert a utc string date and time to a winter paris date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-02'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(localIsoString).toBe('2023-02-02T11:00:00.000+01:00'); + }); + it('should convert a utc string date and time to a summer paris date isostring without dst', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + false, + ); + expect(localIsoString).toBe('2023-06-22T11:00:00.000+01:00'); + }); + it('should convert a utc date to a tonga date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-01'; + const utcTime = '23:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Pacific/Tongatapu', + ); + expect(localIsoString).toBe('2023-02-02T12:00:00.000+13:00'); + }); + it('should convert a utc date to a papeete date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-03'; + const utcTime = '01:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Pacific/Tahiti', + ); + expect(localIsoString).toBe('2023-02-02T15:00:00.000-10:00'); + }); + it('should throw an error if date is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-32'; + const utcTime = '07:00'; + expect(() => { + timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '27:00'; + expect(() => { + timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '07:00'; + expect(() => { + timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Foo/Bar', + ); + }).toThrow(); + }); + }); + + describe('utcUnixEpochDayFromTime', () => { + it('should get the utc day of paris at 12:00', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.utcUnixEpochDayFromTime('12:00', 'Europe/Paris'), + ).toBe(4); + }); + it('should get the utc day of paris at 00:00', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.utcUnixEpochDayFromTime('00:00', 'Europe/Paris'), + ).toBe(3); + }); + it('should get the utc day of papeete at 16:00', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.utcUnixEpochDayFromTime('16:00', 'Pacific/Tahiti'), + ).toBe(5); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.utcUnixEpochDayFromTime('28:00', 'Europe/Paris'); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.utcUnixEpochDayFromTime('12:00', 'Foo/Bar'); + }).toThrow(); + }); + }); + + describe('localUnixEpochDayFromTime', () => { + it('should get the day of paris at 12:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('12:00', 'Europe/Paris'), + ).toBe(4); + }); + it('should get the day of paris at 23:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('23:00', 'Europe/Paris'), + ).toBe(5); + }); + it('should get the day of papeete at 05:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('05:00', 'Pacific/Tahiti'), + ).toBe(3); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.localUnixEpochDayFromTime('28:00', 'Europe/Paris'); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.localUnixEpochDayFromTime('12:00', 'Foo/Bar'); + }).toThrow(); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts b/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts new file mode 100644 index 0000000..46e3ab8 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts @@ -0,0 +1,14 @@ +import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder'; + +describe('Timezone Finder', () => { + it('should be defined', () => { + const timezoneFinder: TimezoneFinder = new TimezoneFinder(); + expect(timezoneFinder).toBeDefined(); + }); + it('should get timezone for Nancy(France) as Europe/Paris', () => { + const timezoneFinder: TimezoneFinder = new TimezoneFinder(); + 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/interface/has-day.decorator.spec.ts b/src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts similarity index 89% rename from src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts rename to src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts index 50c29e8..ef27a62 100644 --- a/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts +++ b/src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts @@ -1,6 +1,6 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; -import { ScheduleItemDto } from '@modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto'; -import { HasDay } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator'; +import { ScheduleItemDto } from '@modules/ad/interface/grpc-controllers/dtos/schedule-item.dto'; +import { HasDay } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator'; import { Validator } from 'class-validator'; describe('Has day decorator', () => { diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts similarity index 87% rename from src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts rename to src/modules/ad/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts index 54fb8d1..bf61ce6 100644 --- a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts +++ b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts @@ -1,5 +1,5 @@ -import { HasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator'; -import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { HasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { Validator } from 'class-validator'; describe('valid position indexes decorator', () => { diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.validator.spec.ts similarity index 87% rename from src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts rename to src/modules/ad/tests/unit/interface/has-valid-position-indexes.validator.spec.ts index aed3a13..98e9d24 100644 --- a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts +++ b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.validator.spec.ts @@ -1,5 +1,5 @@ -import { hasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator'; -import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { hasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; describe('Waypoint position validator', () => { const mockAddress1: WaypointDto = { diff --git a/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts b/src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts similarity index 92% rename from src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts rename to src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts index b388a2c..71ab685 100644 --- a/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts +++ b/src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts @@ -1,4 +1,4 @@ -import { IsAfterOrEqual } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator'; +import { IsAfterOrEqual } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator'; import { Validator } from 'class-validator'; describe('Is after or equal decorator', () => { diff --git a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts similarity index 81% rename from src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts rename to src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 894bda6..23ac483 100644 --- a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -1,8 +1,9 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; -import { Frequency } from '@modules/matcher/core/domain/match.types'; -import { MatchRequestDto } from '@modules/matcher/interface/grpc-controllers/dtos/match.request.dto'; -import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; -import { MatchGrpcController } from '@modules/matcher/interface/grpc-controllers/match.grpc-controller'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; +import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller'; import { QueryBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; @@ -27,6 +28,7 @@ const destinationWaypoint: WaypointDto = { }; const punctualMatchRequestDto: MatchRequestDto = { + driver: false, passenger: true, frequency: Frequency.PUNCTUAL, fromDate: '2023-08-15', @@ -34,9 +36,13 @@ const punctualMatchRequestDto: MatchRequestDto = { schedule: [ { time: '07:00', + day: 2, + margin: 900, }, ], waypoints: [originWaypoint, destinationWaypoint], + strict: false, + algorithmType: AlgorithmType.PASSENGER_ORIENTED, }; const mockQueryBus = { diff --git a/src/modules/matcher/core/application/types/coordinates.ts b/src/modules/matcher/core/application/types/coordinates.ts deleted file mode 100644 index 8e149ed..0000000 --- a/src/modules/matcher/core/application/types/coordinates.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Coordinates = { - lon: number; - lat: number; -}; diff --git a/src/modules/matcher/core/application/types/schedule-item.ts b/src/modules/matcher/core/application/types/schedule-item.ts deleted file mode 100644 index a40e06d..0000000 --- a/src/modules/matcher/core/application/types/schedule-item.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ScheduleItem = { - day?: number; - time: string; - margin?: number; -}; diff --git a/src/modules/matcher/core/application/types/waypoint.ts b/src/modules/matcher/core/application/types/waypoint.ts deleted file mode 100644 index c73e024..0000000 --- a/src/modules/matcher/core/application/types/waypoint.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Address } from './address'; - -export type Waypoint = { - position?: number; -} & Address; diff --git a/src/modules/matcher/core/domain/match.types.ts b/src/modules/matcher/core/domain/match.types.ts deleted file mode 100644 index 89be65e..0000000 --- a/src/modules/matcher/core/domain/match.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum Frequency { - PUNCTUAL = 'PUNCTUAL', - RECURRENT = 'RECURRENT', -} - -export enum AlgorithmType { - CLASSIC = 'CLASSIC', -} From cde1760099ec195328dc95b87e4cefe8b30f59ac Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 30 Aug 2023 14:07:00 +0200 Subject: [PATCH 06/52] add default params, time transformer --- .../application/ports/ad.repository.port.ts | 21 +--- .../application/ports/default-params.type.ts | 12 +- .../queries/match/match.query-handler.ts | 33 ++++- .../application/queries/match/match.query.ts | 115 ++++++++++++++++-- .../application/types/schedule-item.type.ts | 4 +- src/modules/ad/core/domain/match.types.ts | 19 +++ .../schedule-item.value-object.ts | 10 +- .../ad/infrastructure/ad.repository.ts | 10 +- src/modules/ad/infrastructure/ad.selector.ts | 4 +- .../infrastructure/default-params-provider.ts | 74 +++++++++-- .../input-datetime-transformer.ts | 2 +- .../dtos/match.request.dto.ts | 89 +++++++------- .../unit/core/match.query-handler.spec.ts | 40 +++++- .../core/passenger-oriented-algorithm.spec.ts | 11 +- .../default-param.provider.spec.ts | 28 ++++- .../input-datetime-transformer.spec.ts | 11 +- 16 files changed, 377 insertions(+), 106 deletions(-) diff --git a/src/modules/ad/core/application/ports/ad.repository.port.ts b/src/modules/ad/core/application/ports/ad.repository.port.ts index af68529..8e9674b 100644 --- a/src/modules/ad/core/application/ports/ad.repository.port.ts +++ b/src/modules/ad/core/application/ports/ad.repository.port.ts @@ -1,23 +1,8 @@ import { ExtendedRepositoryPort } from '@mobicoop/ddd-library'; import { AdEntity } from '../../domain/ad.entity'; -import { AlgorithmType, Candidate } from '../types/algorithm.types'; -import { Frequency } from '../../domain/ad.types'; +import { Candidate } from '../types/algorithm.types'; +import { MatchQuery } from '../queries/match/match.query'; export type AdRepositoryPort = ExtendedRepositoryPort & { - getCandidates(query: CandidateQuery): Promise; -}; - -export type CandidateQuery = { - driver: boolean; - passenger: boolean; - strict: boolean; - frequency: Frequency; - fromDate: string; - toDate: string; - schedule: { - day?: number; - time: string; - margin?: number; - }[]; - algorithmType: AlgorithmType; + getCandidates(query: MatchQuery): Promise; }; diff --git a/src/modules/ad/core/application/ports/default-params.type.ts b/src/modules/ad/core/application/ports/default-params.type.ts index dbf0798..3bee195 100644 --- a/src/modules/ad/core/application/ports/default-params.type.ts +++ b/src/modules/ad/core/application/ports/default-params.type.ts @@ -1,3 +1,5 @@ +import { AlgorithmType } from '../types/algorithm.types'; + export type DefaultParams = { DRIVER: boolean; PASSENGER: boolean; @@ -5,5 +7,13 @@ export type DefaultParams = { SEATS_REQUESTED: number; DEPARTURE_TIME_MARGIN: number; STRICT: boolean; - DEFAULT_TIMEZONE: string; + TIMEZONE: string; + ALGORITHM_TYPE: AlgorithmType; + REMOTENESS: number; + USE_PROPORTION: boolean; + PROPORTION: number; + USE_AZIMUTH: boolean; + AZIMUTH_MARGIN: number; + MAX_DETOUR_DISTANCE_RATIO: number; + MAX_DETOUR_DURATION_RATIO: number; }; diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 404402f..a0fe915 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -5,15 +5,44 @@ import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm'; import { AlgorithmType } from '../../types/algorithm.types'; import { Inject } from '@nestjs/common'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; -import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port'; +import { DefaultParams } from '../../ports/default-params.type'; @QueryHandler(MatchQuery) export class MatchQueryHandler implements IQueryHandler { + private readonly _defaultParams: DefaultParams; + constructor( + @Inject(PARAMS_PROVIDER) + private readonly defaultParamsProvider: DefaultParamsProviderPort, @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, - ) {} + ) { + this._defaultParams = defaultParamsProvider.getParams(); + } + execute = async (query: MatchQuery): Promise => { + query + .setMissingMarginDurations(this._defaultParams.DEPARTURE_TIME_MARGIN) + .setMissingStrict(this._defaultParams.STRICT) + .setDefaultDriverAndPassengerParameters({ + driver: this._defaultParams.DRIVER, + passenger: this._defaultParams.PASSENGER, + seatsProposed: this._defaultParams.SEATS_PROPOSED, + seatsRequested: this._defaultParams.SEATS_REQUESTED, + }) + .setDefaultAlgorithmParameters({ + algorithmType: this._defaultParams.ALGORITHM_TYPE, + remoteness: 0, + useProportion: false, + proportion: 0, + useAzimuth: false, + azimuthMargin: 0, + maxDetourDistanceRatio: 0, + maxDetourDurationRatio: 0, + }); + let algorithm: Algorithm; switch (query.algorithmType) { case AlgorithmType.PASSENGER_ORIENTED: diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 3b0d480..d2348ae 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -1,21 +1,32 @@ import { QueryBase } from '@mobicoop/ddd-library'; import { AlgorithmType } from '../../types/algorithm.types'; import { Waypoint } from '../../types/waypoint.type'; -import { ScheduleItem } from '../../types/schedule-item.type'; import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; export class MatchQuery extends QueryBase { - readonly driver: boolean; - readonly passenger: boolean; + driver?: boolean; + passenger?: boolean; readonly frequency: Frequency; readonly fromDate: string; readonly toDate: string; - readonly schedule: ScheduleItem[]; - readonly strict: boolean; + schedule: ScheduleItem[]; + seatsProposed?: number; + seatsRequested?: number; + strict?: boolean; readonly waypoints: Waypoint[]; - readonly algorithmType: AlgorithmType; + algorithmType?: AlgorithmType; + remoteness?: number; + useProportion?: boolean; + proportion?: number; + useAzimuth?: boolean; + azimuthMargin?: number; + maxDetourDistanceRatio?: number; + maxDetourDurationRatio?: number; + readonly page?: number; + readonly perPage?: number; - constructor(props: MatchQuery) { + constructor(props: MatchRequestDto) { super(); this.driver = props.driver; this.passenger = props.passenger; @@ -23,8 +34,98 @@ export class MatchQuery extends QueryBase { this.fromDate = props.fromDate; this.toDate = props.toDate; this.schedule = props.schedule; + this.seatsProposed = props.seatsProposed; + this.seatsRequested = props.seatsRequested; this.strict = props.strict; this.waypoints = props.waypoints; this.algorithmType = props.algorithmType; + this.remoteness = props.remoteness; + this.useProportion = props.useProportion; + this.proportion = props.proportion; + this.useAzimuth = props.useAzimuth; + this.azimuthMargin = props.azimuthMargin; + this.maxDetourDistanceRatio = props.maxDetourDistanceRatio; + this.maxDetourDurationRatio = props.maxDetourDurationRatio; + this.page = props.page ?? 1; + this.perPage = props.perPage ?? 10; } + + setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => { + this.schedule.forEach((day: ScheduleItem) => { + if (day.margin === undefined) day.margin = defaultMarginDuration; + }); + return this; + }; + + setMissingStrict = (strict: boolean): MatchQuery => { + if (this.strict === undefined) this.strict = strict; + return this; + }; + + setDefaultDriverAndPassengerParameters = ( + defaultDriverAndPassengerParameters: DefaultDriverAndPassengerParameters, + ): MatchQuery => { + this.driver = !!this.driver; + this.passenger = !!this.passenger; + if (!this.driver && !this.passenger) { + this.driver = defaultDriverAndPassengerParameters.driver; + this.seatsProposed = defaultDriverAndPassengerParameters.seatsProposed; + this.passenger = defaultDriverAndPassengerParameters.passenger; + this.seatsRequested = defaultDriverAndPassengerParameters.seatsRequested; + return this; + } + if (!this.seatsProposed || this.seatsProposed <= 0) + this.seatsProposed = defaultDriverAndPassengerParameters.seatsProposed; + if (!this.seatsRequested || this.seatsRequested <= 0) + this.seatsRequested = defaultDriverAndPassengerParameters.seatsRequested; + return this; + }; + + setDefaultAlgorithmParameters = ( + defaultAlgorithmParameters: DefaultAlgorithmParameters, + ): MatchQuery => { + if (!this.algorithmType) + this.algorithmType = defaultAlgorithmParameters.algorithmType; + if (!this.remoteness) + this.remoteness = defaultAlgorithmParameters.remoteness; + if (this.useProportion == undefined) + this.useProportion = defaultAlgorithmParameters.useProportion; + if (!this.proportion) + this.proportion = defaultAlgorithmParameters.proportion; + if (this.useAzimuth == undefined) + this.useAzimuth = defaultAlgorithmParameters.useAzimuth; + if (!this.azimuthMargin) + this.azimuthMargin = defaultAlgorithmParameters.azimuthMargin; + if (!this.maxDetourDistanceRatio) + this.maxDetourDistanceRatio = + defaultAlgorithmParameters.maxDetourDistanceRatio; + if (!this.maxDetourDurationRatio) + this.maxDetourDurationRatio = + defaultAlgorithmParameters.maxDetourDurationRatio; + return this; + }; +} + +type ScheduleItem = { + day?: number; + time: string; + margin?: number; +}; + +interface DefaultDriverAndPassengerParameters { + driver: boolean; + passenger: boolean; + seatsProposed: number; + seatsRequested: number; +} + +interface DefaultAlgorithmParameters { + algorithmType: AlgorithmType; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDistanceRatio: number; + maxDetourDurationRatio: number; } diff --git a/src/modules/ad/core/application/types/schedule-item.type.ts b/src/modules/ad/core/application/types/schedule-item.type.ts index a40e06d..92dab99 100644 --- a/src/modules/ad/core/application/types/schedule-item.type.ts +++ b/src/modules/ad/core/application/types/schedule-item.type.ts @@ -1,5 +1,5 @@ export type ScheduleItem = { - day?: number; + day: number; time: string; - margin?: number; + margin: number; }; diff --git a/src/modules/ad/core/domain/match.types.ts b/src/modules/ad/core/domain/match.types.ts index 997f87b..911029b 100644 --- a/src/modules/ad/core/domain/match.types.ts +++ b/src/modules/ad/core/domain/match.types.ts @@ -1,3 +1,5 @@ +import { AlgorithmType } from '../application/types/algorithm.types'; + // All properties that a Match has export interface MatchProps { adId: string; @@ -7,3 +9,20 @@ export interface MatchProps { export interface CreateMatchProps { adId: string; } + +export interface DefaultMatchQueryProps { + driver: boolean; + passenger: boolean; + marginDuration: number; + strict: boolean; + seatsProposed: number; + seatsRequested: number; + algorithmType?: AlgorithmType; + remoteness?: number; + useProportion?: boolean; + proportion?: number; + useAzimuth?: boolean; + azimuthMargin?: number; + maxDetourDistanceRatio?: number; + maxDetourDurationRatio?: number; +} diff --git a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts index 4a773a4..97fb87f 100644 --- a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts @@ -10,13 +10,13 @@ import { * */ export interface ScheduleItemProps { - day?: number; + day: number; time: string; - margin?: number; + margin: number; } export class ScheduleItem extends ValueObject { - get day(): number | undefined { + get day(): number { return this.props.day; } @@ -24,13 +24,13 @@ export class ScheduleItem extends ValueObject { return this.props.time; } - get margin(): number | undefined { + get margin(): number { return this.props.margin; } // eslint-disable-next-line @typescript-eslint/no-unused-vars protected validate(props: ScheduleItemProps): void { - if (props.day !== undefined && (props.day < 0 || props.day > 6)) + if (props.day < 0 || props.day > 6) throw new ArgumentOutOfRangeException('day must be between 0 and 6'); if (props.time.split(':').length != 2) throw new ArgumentInvalidException('time is invalid'); diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 1a33b8a..551579e 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -1,9 +1,6 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { - AdRepositoryPort, - CandidateQuery, -} from '../core/application/ports/ad.repository.port'; +import { AdRepositoryPort } from '../core/application/ports/ad.repository.port'; import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library'; import { PrismaService } from './prisma.service'; import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; @@ -13,6 +10,7 @@ import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/pris import { Frequency, Role } from '../core/domain/ad.types'; import { Candidate } from '../core/application/types/algorithm.types'; import { AdSelector } from './ad.selector'; +import { MatchQuery } from '../core/application/queries/match/match.query'; export type AdBaseModel = { uuid: string; @@ -93,7 +91,7 @@ export class AdRepository ); } - getCandidates = async (query: CandidateQuery): Promise => { + getCandidates = async (query: MatchQuery): Promise => { // let candidates: Candidate[] = []; const sqlQueries: QueryRole[] = []; if (query.driver) @@ -115,7 +113,7 @@ export class AdRepository } as AdsRole), ), ); - console.log(results[0].ads); + // console.log(results[0].ads); return []; }; } diff --git a/src/modules/ad/infrastructure/ad.selector.ts b/src/modules/ad/infrastructure/ad.selector.ts index 74e91db..821eb52 100644 --- a/src/modules/ad/infrastructure/ad.selector.ts +++ b/src/modules/ad/infrastructure/ad.selector.ts @@ -1,9 +1,9 @@ -import { CandidateQuery } from '../core/application/ports/ad.repository.port'; +import { MatchQuery } from '../core/application/queries/match/match.query'; import { AlgorithmType } from '../core/application/types/algorithm.types'; import { Role } from '../core/domain/ad.types'; export class AdSelector { - static select = (role: Role, query: CandidateQuery): string => { + static select = (role: Role, query: MatchQuery): string => { switch (query.algorithmType) { case AlgorithmType.PASSENGER_ORIENTED: default: diff --git a/src/modules/ad/infrastructure/default-params-provider.ts b/src/modules/ad/infrastructure/default-params-provider.ts index 7244e39..81f1737 100644 --- a/src/modules/ad/infrastructure/default-params-provider.ts +++ b/src/modules/ad/infrastructure/default-params-provider.ts @@ -2,32 +2,84 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; import { DefaultParams } from '../core/application/ports/default-params.type'; +import { AlgorithmType } from '../core/application/types/algorithm.types'; -const DEFAULT_SEATS_PROPOSED = 3; -const DEFAULT_SEATS_REQUESTED = 1; -const DEFAULT_DEPARTURE_TIME_MARGIN = 900; -const DEFAULT_TIMEZONE = 'Europe/Paris'; +const DRIVER = false; +const PASSENGER = true; +const SEATS_PROPOSED = 3; +const SEATS_REQUESTED = 1; +const DEPARTURE_TIME_MARGIN = 900; +const TIMEZONE = 'Europe/Paris'; +const ALGORITHM_TYPE = 'PASSENGER_ORIENTED'; +const REMOTENESS = 15000; +const USE_PROPORTION = true; +const PROPORTION = 0.3; +const USE_AZIMUTH = true; +const AZIMUTH_MARGIN = 10; +const MAX_DETOUR_DISTANCE_RATIO = 0.3; +const MAX_DETOUR_DURATION_RATIO = 0.3; @Injectable() export class DefaultParamsProvider implements DefaultParamsProviderPort { constructor(private readonly _configService: ConfigService) {} getParams = (): DefaultParams => ({ - DRIVER: this._configService.get('ROLE') == 'driver', + DRIVER: + this._configService.get('ROLE') !== undefined + ? this._configService.get('ROLE') == 'driver' + : DRIVER, SEATS_PROPOSED: this._configService.get('SEATS_PROPOSED') !== undefined ? parseInt(this._configService.get('SEATS_PROPOSED') as string) - : DEFAULT_SEATS_PROPOSED, - PASSENGER: this._configService.get('ROLE') == 'passenger', + : SEATS_PROPOSED, + PASSENGER: + this._configService.get('ROLE') !== undefined + ? this._configService.get('ROLE') == 'passenger' + : PASSENGER, SEATS_REQUESTED: this._configService.get('SEATS_REQUESTED') !== undefined ? parseInt(this._configService.get('SEATS_REQUESTED') as string) - : DEFAULT_SEATS_REQUESTED, + : SEATS_REQUESTED, DEPARTURE_TIME_MARGIN: this._configService.get('DEPARTURE_TIME_MARGIN') !== undefined ? parseInt(this._configService.get('DEPARTURE_TIME_MARGIN') as string) - : DEFAULT_DEPARTURE_TIME_MARGIN, + : DEPARTURE_TIME_MARGIN, STRICT: this._configService.get('STRICT_FREQUENCY') == 'true', - DEFAULT_TIMEZONE: - this._configService.get('DEFAULT_TIMEZONE') ?? DEFAULT_TIMEZONE, + TIMEZONE: this._configService.get('TIMEZONE') ?? TIMEZONE, + ALGORITHM_TYPE: + AlgorithmType[ + this._configService.get('ALGORITHM_TYPE') as AlgorithmType + ] ?? AlgorithmType[ALGORITHM_TYPE], + REMOTENESS: + this._configService.get('REMOTENESS') !== undefined + ? parseInt(this._configService.get('REMOTENESS') as string) + : REMOTENESS, + USE_PROPORTION: + this._configService.get('USE_PROPORTION') !== undefined + ? this._configService.get('USE_PROPORTION') == 'true' + : USE_PROPORTION, + PROPORTION: + this._configService.get('PROPORTION') !== undefined + ? parseFloat(this._configService.get('PROPORTION') as string) + : PROPORTION, + USE_AZIMUTH: + this._configService.get('USE_AZIMUTH') !== undefined + ? this._configService.get('USE_AZIMUTH') == 'true' + : USE_AZIMUTH, + AZIMUTH_MARGIN: + this._configService.get('AZIMUTH_MARGIN') !== undefined + ? parseInt(this._configService.get('AZIMUTH_MARGIN') as string) + : AZIMUTH_MARGIN, + MAX_DETOUR_DISTANCE_RATIO: + this._configService.get('MAX_DETOUR_DISTANCE_RATIO') !== undefined + ? parseFloat( + this._configService.get('MAX_DETOUR_DISTANCE_RATIO') as string, + ) + : MAX_DETOUR_DISTANCE_RATIO, + MAX_DETOUR_DURATION_RATIO: + this._configService.get('MAX_DETOUR_DURATION_RATIO') !== undefined + ? parseFloat( + this._configService.get('MAX_DETOUR_DURATION_RATIO') as string, + ) + : MAX_DETOUR_DURATION_RATIO, }); } diff --git a/src/modules/ad/infrastructure/input-datetime-transformer.ts b/src/modules/ad/infrastructure/input-datetime-transformer.ts index faa4025..97df366 100644 --- a/src/modules/ad/infrastructure/input-datetime-transformer.ts +++ b/src/modules/ad/infrastructure/input-datetime-transformer.ts @@ -23,7 +23,7 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort { private readonly timezoneFinder: TimezoneFinderPort, @Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort, ) { - this._defaultTimezone = defaultParamsProvider.getParams().DEFAULT_TIMEZONE; + this._defaultTimezone = defaultParamsProvider.getParams().TIMEZONE; } /** diff --git a/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts index 86f9826..c073e44 100644 --- a/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts @@ -2,10 +2,13 @@ import { ArrayMinSize, IsArray, IsBoolean, + IsDecimal, IsEnum, IsISO8601, IsInt, IsOptional, + Max, + Min, ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; @@ -18,13 +21,13 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; export class MatchRequestDto { - // @IsOptional() + @IsOptional() @IsBoolean() - driver: boolean; + driver?: boolean; - // @IsOptional() + @IsOptional() @IsBoolean() - passenger: boolean; + passenger?: boolean; @IsEnum(Frequency) @HasDay('schedule', { @@ -53,17 +56,17 @@ export class MatchRequestDto { @ValidateNested({ each: true }) schedule: ScheduleItemDto[]; - // @IsOptional() - // @IsInt() - // seatsProposed?: number; + @IsOptional() + @IsInt() + seatsProposed?: number; - // @IsOptional() - // @IsInt() - // seatsRequested?: number; + @IsOptional() + @IsInt() + seatsRequested?: number; - // @IsOptional() + @IsOptional() @IsBoolean() - strict: boolean; + strict?: boolean; @Type(() => WaypointDto) @IsArray() @@ -72,45 +75,45 @@ export class MatchRequestDto { @ValidateNested({ each: true }) waypoints: WaypointDto[]; - // @IsOptional() + @IsOptional() @IsEnum(AlgorithmType) - algorithmType: AlgorithmType; + algorithmType?: AlgorithmType; - // @IsOptional() - // @IsInt() - // remoteness?: number; + @IsOptional() + @IsInt() + remoteness?: number; - // @IsOptional() - // @IsBoolean() - // useProportion?: boolean; + @IsOptional() + @IsBoolean() + useProportion?: boolean; - // @IsOptional() - // @IsDecimal() - // @Min(0) - // @Max(1) - // proportion?: number; + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + proportion?: number; - // @IsOptional() - // @IsBoolean() - // useAzimuth?: boolean; + @IsOptional() + @IsBoolean() + useAzimuth?: boolean; - // @IsOptional() - // @IsInt() - // @Min(0) - // @Max(359) - // azimuthMargin?: number; + @IsOptional() + @IsInt() + @Min(0) + @Max(359) + azimuthMargin?: number; - // @IsOptional() - // @IsDecimal() - // @Min(0) - // @Max(1) - // maxDetourDistanceRatio?: number; + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + maxDetourDistanceRatio?: number; - // @IsOptional() - // @IsDecimal() - // @Min(0) - // @Max(1) - // maxDetourDurationRatio?: number; + @IsOptional() + @IsDecimal() + @Min(0) + @Max(1) + maxDetourDurationRatio?: number; @IsOptional() @IsInt() diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 98220ac..394cf41 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,9 +1,10 @@ -import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens'; +import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { Test, TestingModule } from '@nestjs/testing'; @@ -27,7 +28,36 @@ const destinationWaypoint: Waypoint = { }; const mockAdRepository = { - getCandidates: jest.fn(), + getCandidates: jest.fn().mockImplementation(() => [ + { + ad: { + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + }, + role: Role.DRIVER, + }, + ]), +}; + +const mockDefaultParamsProvider: DefaultParamsProviderPort = { + getParams: () => { + return { + DEPARTURE_TIME_MARGIN: 900, + DRIVER: false, + SEATS_PROPOSED: 3, + PASSENGER: true, + SEATS_REQUESTED: 1, + STRICT: false, + TIMEZONE: 'Europe/Paris', + ALGORITHM_TYPE: AlgorithmType.PASSENGER_ORIENTED, + 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, + }; + }, }; describe('Match Query Handler', () => { @@ -41,6 +71,10 @@ describe('Match Query Handler', () => { provide: AD_REPOSITORY, useValue: mockAdRepository, }, + { + provide: PARAMS_PROVIDER, + useValue: mockDefaultParamsProvider, + }, ], }).compile(); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index 4aae63c..cc35ac0 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -3,7 +3,7 @@ import { MatchQuery } from '@modules/ad/core/application/queries/match/match.que import { PassengerOrientedAlgorithm } from '@modules/ad/core/application/queries/match/passenger-oriented-algorithm'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; const originWaypoint: Waypoint = { @@ -52,7 +52,14 @@ const mockMatcherRepository: AdRepositoryPort = { count: jest.fn(), healthCheck: jest.fn(), queryRawUnsafe: jest.fn(), - getCandidates: jest.fn(), + getCandidates: jest.fn().mockImplementation(() => [ + { + ad: { + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + }, + role: Role.DRIVER, + }, + ]), }; describe('Passenger oriented algorithm', () => { diff --git a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts index 5d017e5..cfc0b1f 100644 --- a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts @@ -16,8 +16,24 @@ const mockConfigService = { return 1; case 'STRICT_FREQUENCY': return 'false'; - case 'DEFAULT_TIMEZONE': + case 'TIMEZONE': return 'Europe/Paris'; + case 'ALGORITHM_TYPE': + return 'PASSENGER_ORIENTED'; + case 'REMOTENESS': + return 15000; + case 'USE_PROPORTION': + return 'true'; + case 'PROPORTION': + return 0.3; + case 'USE_AZIMUTH': + return 'true'; + case 'AZIMUTH_MARGIN': + return 10; + case 'MAX_DETOUR_DISTANCE_RATIO': + return 0.3; + case 'MAX_DETOUR_DURATION_RATIO': + return 0.3; default: return 'some_default_value'; } @@ -53,6 +69,14 @@ describe('DefaultParamsProvider', () => { expect(params.DEPARTURE_TIME_MARGIN).toBe(900); expect(params.PASSENGER).toBeTruthy(); expect(params.DRIVER).toBeFalsy(); - expect(params.DEFAULT_TIMEZONE).toBe('Europe/Paris'); + expect(params.TIMEZONE).toBe('Europe/Paris'); + expect(params.ALGORITHM_TYPE).toBe('PASSENGER_ORIENTED'); + expect(params.REMOTENESS).toBe(15000); + expect(params.USE_PROPORTION).toBeTruthy(); + expect(params.PROPORTION).toBe(0.3); + expect(params.USE_AZIMUTH).toBeTruthy(); + expect(params.AZIMUTH_MARGIN).toBe(10); + expect(params.MAX_DETOUR_DISTANCE_RATIO).toBe(0.3); + expect(params.MAX_DETOUR_DURATION_RATIO).toBe(0.3); }); }); diff --git a/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts index 11733a0..bc4bce3 100644 --- a/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts @@ -7,6 +7,7 @@ import { Frequency } from '@modules/ad/core/application/ports/datetime-transform import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port'; import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer'; import { Test, TestingModule } from '@nestjs/testing'; @@ -19,7 +20,15 @@ const mockDefaultParamsProvider: DefaultParamsProviderPort = { PASSENGER: true, SEATS_REQUESTED: 1, STRICT: false, - DEFAULT_TIMEZONE: 'Europe/Paris', + TIMEZONE: 'Europe/Paris', + ALGORITHM_TYPE: AlgorithmType.PASSENGER_ORIENTED, + 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, }; }, }; From 657f8e7a032624ff9addde6bb41a7595c3b0205f Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 31 Aug 2023 11:09:56 +0200 Subject: [PATCH 07/52] upgrade tests --- .../queries/match/match.query-handler.ts | 28 ++-- .../application/queries/match/match.query.ts | 60 ++++++++- .../unit/core/match.query-handler.spec.ts | 18 ++- .../ad/tests/unit/core/match.query.spec.ts | 127 ++++++++++++++++++ .../default-param.provider.spec.ts | 69 +++++++--- 5 files changed, 273 insertions(+), 29 deletions(-) create mode 100644 src/modules/ad/tests/unit/core/match.query.spec.ts diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index a0fe915..23cac57 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -5,10 +5,15 @@ import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm'; import { AlgorithmType } from '../../types/algorithm.types'; import { Inject } from '@nestjs/common'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; -import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens'; +import { + AD_REPOSITORY, + INPUT_DATETIME_TRANSFORMER, + PARAMS_PROVIDER, +} from '@modules/ad/ad.di-tokens'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port'; import { DefaultParams } from '../../ports/default-params.type'; +import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; @QueryHandler(MatchQuery) export class MatchQueryHandler implements IQueryHandler { @@ -18,6 +23,8 @@ export class MatchQueryHandler implements IQueryHandler { @Inject(PARAMS_PROVIDER) private readonly defaultParamsProvider: DefaultParamsProviderPort, @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, + @Inject(INPUT_DATETIME_TRANSFORMER) + private readonly datetimeTransformer: DateTimeTransformerPort, ) { this._defaultParams = defaultParamsProvider.getParams(); } @@ -34,14 +41,17 @@ export class MatchQueryHandler implements IQueryHandler { }) .setDefaultAlgorithmParameters({ algorithmType: this._defaultParams.ALGORITHM_TYPE, - remoteness: 0, - useProportion: false, - proportion: 0, - useAzimuth: false, - azimuthMargin: 0, - maxDetourDistanceRatio: 0, - maxDetourDurationRatio: 0, - }); + remoteness: this._defaultParams.REMOTENESS, + useProportion: this._defaultParams.USE_PROPORTION, + proportion: this._defaultParams.PROPORTION, + useAzimuth: this._defaultParams.USE_AZIMUTH, + azimuthMargin: this._defaultParams.AZIMUTH_MARGIN, + maxDetourDistanceRatio: this._defaultParams.MAX_DETOUR_DISTANCE_RATIO, + maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, + }) + .setDatesAndSchedule(this.datetimeTransformer); + + console.log(query); let algorithm: Algorithm; switch (query.algorithmType) { diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index d2348ae..b79e613 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -3,13 +3,14 @@ import { AlgorithmType } from '../../types/algorithm.types'; import { Waypoint } from '../../types/waypoint.type'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; +import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; export class MatchQuery extends QueryBase { driver?: boolean; passenger?: boolean; readonly frequency: Frequency; - readonly fromDate: string; - readonly toDate: string; + fromDate: string; + toDate: string; schedule: ScheduleItem[]; seatsProposed?: number; seatsRequested?: number; @@ -104,6 +105,61 @@ export class MatchQuery extends QueryBase { defaultAlgorithmParameters.maxDetourDurationRatio; return this; }; + + setDatesAndSchedule = ( + datetimeTransformer: DateTimeTransformerPort, + ): MatchQuery => { + this.fromDate = datetimeTransformer.fromDate( + { + date: this.fromDate, + time: this.schedule[0].time, + coordinates: { + lon: this.waypoints[0].lon, + lat: this.waypoints[0].lat, + }, + }, + this.frequency, + ); + this.toDate = datetimeTransformer.toDate( + this.toDate, + { + date: this.fromDate, + time: this.schedule[0].time, + coordinates: { + lon: this.waypoints[0].lon, + lat: this.waypoints[0].lat, + }, + }, + this.frequency, + ); + this.schedule = this.schedule.map((scheduleItem: ScheduleItem) => ({ + day: datetimeTransformer.day( + scheduleItem.day ?? new Date(this.fromDate).getDay(), + { + date: this.fromDate, + time: scheduleItem.time, + coordinates: { + lon: this.waypoints[0].lon, + lat: this.waypoints[0].lat, + }, + }, + this.frequency, + ), + time: datetimeTransformer.time( + { + date: this.fromDate, + time: scheduleItem.time, + coordinates: { + lon: this.waypoints[0].lon, + lat: this.waypoints[0].lat, + }, + }, + this.frequency, + ), + margin: scheduleItem.margin, + })); + return this; + }; } type ScheduleItem = { diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 394cf41..801c3a1 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,4 +1,9 @@ -import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens'; +import { + AD_REPOSITORY, + INPUT_DATETIME_TRANSFORMER, + PARAMS_PROVIDER, +} from '@modules/ad/ad.di-tokens'; +import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; @@ -60,6 +65,13 @@ const mockDefaultParamsProvider: DefaultParamsProviderPort = { }, }; +const mockInputDateTimeTransformer: DateTimeTransformerPort = { + fromDate: jest.fn(), + toDate: jest.fn(), + day: jest.fn(), + time: jest.fn(), +}; + describe('Match Query Handler', () => { let matchQueryHandler: MatchQueryHandler; @@ -75,6 +87,10 @@ describe('Match Query Handler', () => { provide: PARAMS_PROVIDER, useValue: mockDefaultParamsProvider, }, + { + provide: INPUT_DATETIME_TRANSFORMER, + useValue: mockInputDateTimeTransformer, + }, ], }).compile(); diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts new file mode 100644 index 0000000..aea2933 --- /dev/null +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -0,0 +1,127 @@ +import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; +import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const defaultParams: DefaultParams = { + DEPARTURE_TIME_MARGIN: 900, + DRIVER: false, + SEATS_PROPOSED: 3, + PASSENGER: true, + SEATS_REQUESTED: 1, + STRICT: false, + TIMEZONE: 'Europe/Paris', + ALGORITHM_TYPE: AlgorithmType.PASSENGER_ORIENTED, + 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, +}; + +const mockInputDateTimeTransformer: DateTimeTransformerPort = { + fromDate: jest.fn().mockImplementation(() => '2023-08-27'), + toDate: jest.fn().mockImplementation(() => '2023-08-27'), + day: jest.fn().mockImplementation(() => 0), + time: jest.fn().mockImplementation(() => '23:05'), +}; + +describe('Match Query', () => { + it('should set default values', async () => { + const matchQuery = new MatchQuery({ + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }); + matchQuery + .setMissingMarginDurations(defaultParams.DEPARTURE_TIME_MARGIN) + .setMissingStrict(defaultParams.STRICT) + .setDefaultDriverAndPassengerParameters({ + driver: defaultParams.DRIVER, + passenger: defaultParams.PASSENGER, + seatsProposed: defaultParams.SEATS_PROPOSED, + seatsRequested: defaultParams.SEATS_REQUESTED, + }) + .setDefaultAlgorithmParameters({ + algorithmType: defaultParams.ALGORITHM_TYPE, + remoteness: defaultParams.REMOTENESS, + useProportion: defaultParams.USE_PROPORTION, + proportion: defaultParams.PROPORTION, + useAzimuth: defaultParams.USE_AZIMUTH, + azimuthMargin: defaultParams.AZIMUTH_MARGIN, + maxDetourDistanceRatio: defaultParams.MAX_DETOUR_DISTANCE_RATIO, + maxDetourDurationRatio: defaultParams.MAX_DETOUR_DURATION_RATIO, + }) + .setDatesAndSchedule(mockInputDateTimeTransformer); + expect(matchQuery.strict).toBeFalsy(); + expect(matchQuery.driver).toBeFalsy(); + expect(matchQuery.seatsProposed).toBe(3); + expect(matchQuery.seatsRequested).toBe(1); + expect(matchQuery.algorithmType).toBe(AlgorithmType.PASSENGER_ORIENTED); + expect(matchQuery.remoteness).toBe(15000); + expect(matchQuery.useProportion).toBeTruthy(); + expect(matchQuery.proportion).toBe(0.3); + expect(matchQuery.useAzimuth).toBeTruthy(); + expect(matchQuery.azimuthMargin).toBe(10); + expect(matchQuery.maxDetourDistanceRatio).toBe(0.3); + expect(matchQuery.maxDetourDurationRatio).toBe(0.3); + expect(matchQuery.fromDate).toBe('2023-08-27'); + expect(matchQuery.toDate).toBe('2023-08-27'); + expect(matchQuery.schedule[0].day).toBe(0); + expect(matchQuery.schedule[0].time).toBe('23:05'); + expect(matchQuery.schedule[0].margin).toBe(900); + }); + + it('should set good values for seats', async () => { + const matchQuery = new MatchQuery({ + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + seatsProposed: -1, + seatsRequested: -1, + schedule: [ + { + time: '07:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }); + matchQuery.setDefaultDriverAndPassengerParameters({ + driver: defaultParams.DRIVER, + passenger: defaultParams.PASSENGER, + seatsProposed: defaultParams.SEATS_PROPOSED, + seatsRequested: defaultParams.SEATS_REQUESTED, + }); + expect(matchQuery.seatsProposed).toBe(3); + expect(matchQuery.seatsRequested).toBe(1); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts index cfc0b1f..105358a 100644 --- a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts @@ -3,15 +3,15 @@ import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -const mockConfigService = { +const mockConfigServiceWithDefaults = { get: jest.fn().mockImplementation((value: string) => { switch (value) { case 'DEPARTURE_TIME_MARGIN': - return 900; + return 600; case 'ROLE': return 'passenger'; case 'SEATS_PROPOSED': - return 3; + return 2; case 'SEATS_REQUESTED': return 1; case 'STRICT_FREQUENCY': @@ -21,51 +21,86 @@ const mockConfigService = { case 'ALGORITHM_TYPE': return 'PASSENGER_ORIENTED'; case 'REMOTENESS': - return 15000; + return 10000; case 'USE_PROPORTION': return 'true'; case 'PROPORTION': - return 0.3; + return 0.4; case 'USE_AZIMUTH': return 'true'; case 'AZIMUTH_MARGIN': - return 10; + return 15; case 'MAX_DETOUR_DISTANCE_RATIO': - return 0.3; + return 0.5; case 'MAX_DETOUR_DURATION_RATIO': - return 0.3; + return 0.6; default: return 'some_default_value'; } }), }; +const mockConfigServiceWithoutDefaults = { + get: jest.fn(), +}; + describe('DefaultParamsProvider', () => { - let defaultParamsProvider: DefaultParamsProvider; + let defaultParamsProviderWithDefaults: DefaultParamsProvider; + let defaultParamsProviderWithoutDefaults: DefaultParamsProvider; beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ + const moduleWithDefaults: TestingModule = await Test.createTestingModule({ imports: [], providers: [ DefaultParamsProvider, { provide: ConfigService, - useValue: mockConfigService, + useValue: mockConfigServiceWithDefaults, }, ], }).compile(); - defaultParamsProvider = module.get( - DefaultParamsProvider, - ); + defaultParamsProviderWithDefaults = + moduleWithDefaults.get(DefaultParamsProvider); + + const moduleWithoutDefault: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + DefaultParamsProvider, + { + provide: ConfigService, + useValue: mockConfigServiceWithoutDefaults, + }, + ], + }).compile(); + + defaultParamsProviderWithoutDefaults = + moduleWithoutDefault.get(DefaultParamsProvider); }); it('should be defined', () => { - expect(defaultParamsProvider).toBeDefined(); + expect(defaultParamsProviderWithDefaults).toBeDefined(); }); - it('should provide default params', async () => { - const params: DefaultParams = defaultParamsProvider.getParams(); + it('should provide default params if defaults are set', async () => { + const params: DefaultParams = defaultParamsProviderWithDefaults.getParams(); + expect(params.DEPARTURE_TIME_MARGIN).toBe(600); + expect(params.PASSENGER).toBeTruthy(); + expect(params.DRIVER).toBeFalsy(); + expect(params.TIMEZONE).toBe('Europe/Paris'); + expect(params.ALGORITHM_TYPE).toBe('PASSENGER_ORIENTED'); + expect(params.REMOTENESS).toBe(10000); + expect(params.USE_PROPORTION).toBeTruthy(); + expect(params.PROPORTION).toBe(0.4); + expect(params.USE_AZIMUTH).toBeTruthy(); + expect(params.AZIMUTH_MARGIN).toBe(15); + expect(params.MAX_DETOUR_DISTANCE_RATIO).toBe(0.5); + expect(params.MAX_DETOUR_DURATION_RATIO).toBe(0.6); + }); + + it('should provide default params if defaults are not set', async () => { + const params: DefaultParams = + defaultParamsProviderWithoutDefaults.getParams(); expect(params.DEPARTURE_TIME_MARGIN).toBe(900); expect(params.PASSENGER).toBeTruthy(); expect(params.DRIVER).toBeFalsy(); From 8269242d28b523e42ad4a2e05b0359c23aae3bad Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 31 Aug 2023 17:07:34 +0200 Subject: [PATCH 08/52] create dedicated selectors --- .../application/ports/ad.repository.port.ts | 5 +- .../queries/match/algorithm.abstract.ts | 37 +++++- .../match/completer/completer.abstract.ts | 5 +- .../queries/match/filter/filter.abstract.ts | 5 +- .../queries/match/match.query-handler.ts | 2 - .../match/passenger-oriented-algorithm.ts | 52 +------- .../selector/passenger-oriented.selector.ts | 72 +++++++++++ .../core/application/types/algorithm.types.ts | 4 - .../ad/infrastructure/ad.repository.ts | 116 ++++++++++++------ src/modules/ad/tests/unit/ad.mapper.spec.ts | 2 - 10 files changed, 197 insertions(+), 103 deletions(-) create mode 100644 src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts diff --git a/src/modules/ad/core/application/ports/ad.repository.port.ts b/src/modules/ad/core/application/ports/ad.repository.port.ts index 8e9674b..a123015 100644 --- a/src/modules/ad/core/application/ports/ad.repository.port.ts +++ b/src/modules/ad/core/application/ports/ad.repository.port.ts @@ -1,8 +1,7 @@ import { ExtendedRepositoryPort } from '@mobicoop/ddd-library'; import { AdEntity } from '../../domain/ad.entity'; -import { Candidate } from '../types/algorithm.types'; -import { MatchQuery } from '../queries/match/match.query'; +import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; export type AdRepositoryPort = ExtendedRepositoryPort & { - getCandidates(query: MatchQuery): Promise; + getCandidates(queryString: string): Promise; }; diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index 3bd3f82..c5df2d4 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -1,10 +1,11 @@ import { MatchEntity } from '../../../domain/match.entity'; -import { Candidate, Processor } from '../../types/algorithm.types'; +import { Candidate } from '../../types/algorithm.types'; import { MatchQuery } from './match.query'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; export abstract class Algorithm { protected candidates: Candidate[]; + protected selector: Selector; protected processors: Processor[]; constructor( protected readonly query: MatchQuery, @@ -14,5 +15,37 @@ export abstract class Algorithm { /** * Filter candidates that matches the query */ - abstract match(): Promise; + match = async (): Promise => { + this.candidates = await this.selector.select(); + for (const processor of this.processors) { + this.candidates = await processor.execute(this.candidates); + } + return this.candidates.map((candidate: Candidate) => + MatchEntity.create({ adId: candidate.ad.id }), + ); + }; +} + +/** + * A selector queries potential candidates in a repository + */ +export abstract class Selector { + protected readonly query: MatchQuery; + protected readonly repository: AdRepositoryPort; + constructor(query: MatchQuery, repository: AdRepositoryPort) { + this.query = query; + this.repository = repository; + } + abstract select(): Promise; +} + +/** + * A processor processes candidates information + */ +export abstract class Processor { + protected readonly query: MatchQuery; + constructor(query: MatchQuery) { + this.query = query; + } + abstract execute(candidates: Candidate[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts index 9034bcc..883edd0 100644 --- a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts +++ b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts @@ -1,6 +1,7 @@ -import { Candidate, Processor } from '../../../types/algorithm.types'; +import { Candidate } from '../../../types/algorithm.types'; +import { Processor } from '../algorithm.abstract'; -export abstract class Completer implements Processor { +export abstract class Completer extends Processor { execute = async (candidates: Candidate[]): Promise => this.complete(candidates); diff --git a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts index 5461f33..8262592 100644 --- a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts +++ b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts @@ -1,6 +1,7 @@ -import { Candidate, Processor } from '../../../types/algorithm.types'; +import { Candidate } from '../../../types/algorithm.types'; +import { Processor } from '../algorithm.abstract'; -export abstract class Filter implements Processor { +export abstract class Filter extends Processor { execute = async (candidates: Candidate[]): Promise => this.filter(candidates); diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 23cac57..86a3b17 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -51,8 +51,6 @@ export class MatchQueryHandler implements IQueryHandler { }) .setDatesAndSchedule(this.datetimeTransformer); - console.log(query); - let algorithm: Algorithm; switch (query.algorithmType) { case AlgorithmType.PASSENGER_ORIENTED: diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index 16ac2e7..8ca7147 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -3,9 +3,7 @@ import { MatchQuery } from './match.query'; import { PassengerOrientedWaypointsCompleter } from './completer/passenger-oriented-waypoints.completer'; import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; -import { Role } from '@modules/ad/core/domain/ad.types'; -import { MatchEntity } from '@modules/ad/core/domain/match.entity'; -import { Candidate } from '../../types/algorithm.types'; +import { PassengerOrientedSelector } from './selector/passenger-oriented.selector'; export class PassengerOrientedAlgorithm extends Algorithm { constructor( @@ -13,52 +11,10 @@ export class PassengerOrientedAlgorithm extends Algorithm { protected readonly repository: AdRepositoryPort, ) { super(query, repository); + this.selector = new PassengerOrientedSelector(query, repository); this.processors = [ - new PassengerOrientedWaypointsCompleter(), - new PassengerOrientedGeoFilter(), - ]; - this.candidates = [ - { - ad: { - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - }, - role: Role.DRIVER, - }, + new PassengerOrientedWaypointsCompleter(query), + new PassengerOrientedGeoFilter(query), ]; } - // this.candidates = ( - // await Promise.all( - // sqlQueries.map( - // async (queryRole: QueryRole) => - // ({ - // ads: (await this.repository.queryRawUnsafe( - // queryRole.query, - // )) as AdEntity[], - // role: queryRole.role, - // } as AdsRole), - // ), - // ) - // ) - // .map((adsRole: AdsRole) => - // adsRole.ads.map( - // (adEntity: AdEntity) => - // { - // ad: { - // id: adEntity.id, - // }, - // role: adsRole.role, - // }, - // ), - // ) - // .flat(); - - match = async (): Promise => { - this.candidates = await this.repository.getCandidates(this.query); - for (const processor of this.processors) { - this.candidates = await processor.execute(this.candidates); - } - return this.candidates.map((candidate: Candidate) => - MatchEntity.create({ adId: candidate.ad.id }), - ); - }; } diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts new file mode 100644 index 0000000..2f20b57 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -0,0 +1,72 @@ +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Candidate } from '../../../types/algorithm.types'; +import { Selector } from '../algorithm.abstract'; +import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; + +export class PassengerOrientedSelector extends Selector { + select = async (): Promise => { + const queryStringRoles: QueryStringRole[] = []; + if (this.query.driver) + queryStringRoles.push({ + query: this.asDriverQueryString(), + role: Role.DRIVER, + }); + if (this.query.passenger) + queryStringRoles.push({ + query: this.asPassengerQueryString(), + role: Role.PASSENGER, + }); + + return ( + await Promise.all( + queryStringRoles.map(async (queryStringRole: QueryStringRole) => ({ + ads: await this.repository.getCandidates(queryStringRole.query), + role: queryStringRole.role, + })), + ) + ) + .map((adsRole) => + adsRole.ads.map( + (adReadModel: AdReadModel) => + { + ad: { + id: adReadModel.uuid, + }, + role: adsRole.role, + }, + ), + ) + .flat(); + }; + + private asPassengerQueryString = (): string => `SELECT + ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, + "fromDate","toDate", + "seatsProposed","seatsRequested", + strict, + "driverDuration","driverDistance", + "passengerDuration","passengerDistance", + "fwdAzimuth","backAzimuth", + si.day,si.time,si.margin + FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" + WHERE driver=True`; + + private asDriverQueryString = (): string => `SELECT + ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, + "fromDate","toDate", + "seatsProposed","seatsRequested", + strict, + "driverDuration","driverDistance", + "passengerDuration","passengerDistance", + "fwdAzimuth","backAzimuth", + si.day,si.time,si.margin + FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" + WHERE passenger=True`; + + // await this.repository.getCandidates(this.query); +} + +export type QueryStringRole = { + query: string; + role: Role; +}; diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts index 017f6a4..a3e75a8 100644 --- a/src/modules/ad/core/application/types/algorithm.types.ts +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -4,10 +4,6 @@ export enum AlgorithmType { PASSENGER_ORIENTED = 'PASSENGER_ORIENTED', } -export interface Processor { - execute(candidates: Candidate[]): Promise; -} - export type Candidate = { ad: Ad; role: Role; diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 551579e..a0d220c 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -7,10 +7,7 @@ import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; import { AdEntity } from '../core/domain/ad.entity'; import { AdMapper } from '../ad.mapper'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; -import { Frequency, Role } from '../core/domain/ad.types'; -import { Candidate } from '../core/application/types/algorithm.types'; -import { AdSelector } from './ad.selector'; -import { MatchQuery } from '../core/application/queries/match/match.query'; +import { Frequency } from '../core/domain/ad.types'; export type AdBaseModel = { uuid: string; @@ -34,7 +31,6 @@ export type AdBaseModel = { export type AdReadModel = AdBaseModel & { waypoints: string; - direction: string; schedule: ScheduleItemModel[]; }; @@ -58,6 +54,39 @@ export type ScheduleItemModel = { updatedAt: Date; }; +export type RawAdBaseModel = { + uuid: string; + driver: boolean; + passenger: boolean; + frequency: Frequency; + fromDate: Date; + toDate: Date; + seatsProposed: number; + seatsRequested: number; + strict: boolean; + driverDuration?: number; + driverDistance?: number; + passengerDuration?: number; + passengerDistance?: number; + fwdAzimuth: number; + backAzimuth: number; + waypoints: string; + createdAt: Date; + updatedAt: Date; +}; + +export type RawScheduleItemModel = { + day: number; + time: Date; + margin: number; +}; + +export type RawAdModel = RawAdBaseModel & RawScheduleItemModel; + +export type RawAdReadModel = RawAdBaseModel & { + schedule: RawScheduleItemModel[]; +}; + /** * Repository is used for retrieving/saving domain entities * */ @@ -91,39 +120,50 @@ export class AdRepository ); } - getCandidates = async (query: MatchQuery): Promise => { - // let candidates: Candidate[] = []; - const sqlQueries: QueryRole[] = []; - if (query.driver) - sqlQueries.push({ - query: AdSelector.select(Role.DRIVER, query), - role: Role.DRIVER, - }); - if (query.passenger) - sqlQueries.push({ - query: AdSelector.select(Role.PASSENGER, query), - role: Role.PASSENGER, - }); - const results = await Promise.all( - sqlQueries.map( - async (queryRole: QueryRole) => - ({ - ads: (await this.queryRawUnsafe(queryRole.query)) as AdEntity[], - role: queryRole.role, - } as AdsRole), - ), + getCandidates = async (queryString: string): Promise => + this.toReadModels((await this.queryRawUnsafe(queryString)) as RawAdModel[]); + + private toReadModels = (rawAds: RawAdModel[]): AdReadModel[] => { + const rawAdReadModels: RawAdReadModel[] = rawAds.map( + (rawAd: RawAdModel) => ({ + uuid: rawAd.uuid, + driver: rawAd.driver, + passenger: rawAd.passenger, + frequency: rawAd.frequency, + fromDate: rawAd.fromDate, + toDate: rawAd.toDate, + schedule: [ + { + day: rawAd.day, + time: rawAd.time, + margin: rawAd.margin, + }, + ], + seatsProposed: rawAd.seatsProposed, + seatsRequested: rawAd.seatsRequested, + strict: rawAd.strict, + driverDuration: rawAd.driverDuration, + driverDistance: rawAd.driverDistance, + passengerDuration: rawAd.passengerDuration, + passengerDistance: rawAd.passengerDistance, + fwdAzimuth: rawAd.fwdAzimuth, + backAzimuth: rawAd.backAzimuth, + waypoints: rawAd.waypoints, + createdAt: rawAd.createdAt, + updatedAt: rawAd.updatedAt, + }), ); - // console.log(results[0].ads); - return []; + const adReadModels: AdReadModel[] = []; + rawAdReadModels.forEach((adReadModel: AdReadModel) => { + const ad: AdReadModel | undefined = adReadModels.find( + (arm: AdReadModel) => arm.uuid == adReadModel.uuid, + ); + if (ad) { + ad.schedule.push(...adReadModel.schedule); + } else { + adReadModels.push(adReadModel); + } + }); + return adReadModels; }; } - -type QueryRole = { - query: string; - role: Role; -}; - -type AdsRole = { - ads: AdEntity[]; - role: Role; -}; diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index 925961a..965ff6a 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -84,8 +84,6 @@ const adReadModel: AdReadModel = { }, ], waypoints: "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'", - direction: - "'LINESTRING(6.1765102 48.689445,5.12345 48.76543,2.3522 48.8566)'", driverDistance: 350000, driverDuration: 14400, passengerDistance: 350000, From f4097e96eb8e0576be0a75add2854eb6ad6eb05b Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 31 Aug 2023 17:15:42 +0200 Subject: [PATCH 09/52] remove unused file --- src/modules/ad/infrastructure/ad.selector.ts | 23 -------------------- 1 file changed, 23 deletions(-) delete mode 100644 src/modules/ad/infrastructure/ad.selector.ts diff --git a/src/modules/ad/infrastructure/ad.selector.ts b/src/modules/ad/infrastructure/ad.selector.ts deleted file mode 100644 index 821eb52..0000000 --- a/src/modules/ad/infrastructure/ad.selector.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MatchQuery } from '../core/application/queries/match/match.query'; -import { AlgorithmType } from '../core/application/types/algorithm.types'; -import { Role } from '../core/domain/ad.types'; - -export class AdSelector { - static select = (role: Role, query: MatchQuery): string => { - switch (query.algorithmType) { - case AlgorithmType.PASSENGER_ORIENTED: - default: - return `SELECT - ad.uuid,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, - "fromDate","toDate", - "seatsProposed","seatsRequested", - strict, - "driverDuration","driverDistance", - "passengerDuration","passengerDistance", - "fwdAzimuth","backAzimuth", - si.day,si.time,si.margin - FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" - WHERE driver=True`; - } - }; -} From 717d047aa80525e0d7ef7bf2cd986f4bb2f5921b Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 4 Sep 2023 10:15:02 +0200 Subject: [PATCH 10/52] get candidates in ad repository --- src/modules/ad/ad.mapper.ts | 6 +- .../commands/create-ad/create-ad.service.ts | 2 +- .../selector/passenger-oriented.selector.ts | 2 - .../ad/infrastructure/ad.repository.ts | 110 +++++----- src/modules/ad/tests/unit/ad.mapper.spec.ts | 5 +- .../tests/unit/core/create-ad.service.spec.ts | 2 +- .../core/passenger-oriented-algorithm.spec.ts | 3 +- .../unit/infrastructure/ad.repository.spec.ts | 189 ++++++++++++++++-- 8 files changed, 227 insertions(+), 92 deletions(-) diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 75df1d5..731bf73 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -4,7 +4,7 @@ import { AdWriteModel, AdReadModel, ScheduleItemModel, - AdUnsupportedWriteModel, + AdWriteExtraModel, } from './infrastructure/ad.repository'; import { v4 } from 'uuid'; import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; @@ -26,7 +26,7 @@ export class AdMapper AdEntity, AdReadModel, AdWriteModel, - AdUnsupportedWriteModel, + AdWriteExtraModel, undefined > { @@ -119,7 +119,7 @@ export class AdMapper return entity; }; - toUnsupportedPersistence = (entity: AdEntity): AdUnsupportedWriteModel => ({ + toPersistenceExtra = (entity: AdEntity): AdWriteExtraModel => ({ waypoints: this.directionEncoder.encode(entity.getProps().waypoints), direction: this.directionEncoder.encode(entity.getProps().points), }); diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 2ec8f5a..2e5b606 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -49,7 +49,7 @@ export class CreateAdService implements ICommandHandler { }); try { - await this.repository.insertWithUnsupportedFields(ad, 'ad'); + await this.repository.insertExtra(ad, 'ad'); return ad.id; } catch (error: any) { if (error instanceof ConflictException) { diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 2f20b57..86ae892 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -62,8 +62,6 @@ export class PassengerOrientedSelector extends Selector { si.day,si.time,si.margin FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" WHERE passenger=True`; - - // await this.repository.getCandidates(this.query); } export type QueryStringRole = { diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index a0d220c..e29cde9 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -9,7 +9,7 @@ import { AdMapper } from '../ad.mapper'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; import { Frequency } from '../core/domain/ad.types'; -export type AdBaseModel = { +export type AdModel = { uuid: string; driver: boolean; passenger: boolean; @@ -29,62 +29,42 @@ export type AdBaseModel = { updatedAt: Date; }; -export type AdReadModel = AdBaseModel & { +export type AdReadModel = AdModel & { waypoints: string; schedule: ScheduleItemModel[]; }; -export type AdWriteModel = AdBaseModel & { +export type AdWriteModel = AdModel & { schedule: { create: ScheduleItemModel[]; }; }; -export type AdUnsupportedWriteModel = { +export type AdWriteExtraModel = { waypoints: string; direction: string; }; -export type ScheduleItemModel = { - uuid: string; +export type ScheduleItem = { day: number; time: Date; margin: number; +}; + +export type ScheduleItemModel = ScheduleItem & { + uuid: string; createdAt: Date; updatedAt: Date; }; -export type RawAdBaseModel = { - uuid: string; - driver: boolean; - passenger: boolean; - frequency: Frequency; - fromDate: Date; - toDate: Date; - seatsProposed: number; - seatsRequested: number; - strict: boolean; - driverDuration?: number; - driverDistance?: number; - passengerDuration?: number; - passengerDistance?: number; - fwdAzimuth: number; - backAzimuth: number; +export type UngroupedAdModel = AdModel & + ScheduleItem & { + waypoints: string; + }; + +export type GroupedAdModel = AdModel & { + schedule: ScheduleItem[]; waypoints: string; - createdAt: Date; - updatedAt: Date; -}; - -export type RawScheduleItemModel = { - day: number; - time: Date; - margin: number; -}; - -export type RawAdModel = RawAdBaseModel & RawScheduleItemModel; - -export type RawAdReadModel = RawAdBaseModel & { - schedule: RawScheduleItemModel[]; }; /** @@ -96,7 +76,7 @@ export class AdRepository AdEntity, AdReadModel, AdWriteModel, - AdUnsupportedWriteModel + AdWriteExtraModel > implements AdRepositoryPort { @@ -121,40 +101,44 @@ export class AdRepository } getCandidates = async (queryString: string): Promise => - this.toReadModels((await this.queryRawUnsafe(queryString)) as RawAdModel[]); + this.toAdReadModels( + (await this.prismaRaw.$queryRawUnsafe(queryString)) as UngroupedAdModel[], + ); - private toReadModels = (rawAds: RawAdModel[]): AdReadModel[] => { - const rawAdReadModels: RawAdReadModel[] = rawAds.map( - (rawAd: RawAdModel) => ({ - uuid: rawAd.uuid, - driver: rawAd.driver, - passenger: rawAd.passenger, - frequency: rawAd.frequency, - fromDate: rawAd.fromDate, - toDate: rawAd.toDate, + private toAdReadModels = ( + ungroupedAds: UngroupedAdModel[], + ): AdReadModel[] => { + const groupedAdModels: GroupedAdModel[] = ungroupedAds.map( + (ungroupedAd: UngroupedAdModel) => ({ + uuid: ungroupedAd.uuid, + driver: ungroupedAd.driver, + passenger: ungroupedAd.passenger, + frequency: ungroupedAd.frequency, + fromDate: ungroupedAd.fromDate, + toDate: ungroupedAd.toDate, schedule: [ { - day: rawAd.day, - time: rawAd.time, - margin: rawAd.margin, + day: ungroupedAd.day, + time: ungroupedAd.time, + margin: ungroupedAd.margin, }, ], - seatsProposed: rawAd.seatsProposed, - seatsRequested: rawAd.seatsRequested, - strict: rawAd.strict, - driverDuration: rawAd.driverDuration, - driverDistance: rawAd.driverDistance, - passengerDuration: rawAd.passengerDuration, - passengerDistance: rawAd.passengerDistance, - fwdAzimuth: rawAd.fwdAzimuth, - backAzimuth: rawAd.backAzimuth, - waypoints: rawAd.waypoints, - createdAt: rawAd.createdAt, - updatedAt: rawAd.updatedAt, + seatsProposed: ungroupedAd.seatsProposed, + seatsRequested: ungroupedAd.seatsRequested, + strict: ungroupedAd.strict, + driverDuration: ungroupedAd.driverDuration, + driverDistance: ungroupedAd.driverDistance, + passengerDuration: ungroupedAd.passengerDuration, + passengerDistance: ungroupedAd.passengerDistance, + fwdAzimuth: ungroupedAd.fwdAzimuth, + backAzimuth: ungroupedAd.backAzimuth, + waypoints: ungroupedAd.waypoints, + createdAt: ungroupedAd.createdAt, + updatedAt: ungroupedAd.updatedAt, }), ); const adReadModels: AdReadModel[] = []; - rawAdReadModels.forEach((adReadModel: AdReadModel) => { + groupedAdModels.forEach((adReadModel: AdReadModel) => { const ad: AdReadModel | undefined = adReadModels.find( (arm: AdReadModel) => arm.uuid == adReadModel.uuid, ); diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index 965ff6a..7e93238 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -4,7 +4,7 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AdReadModel, - AdUnsupportedWriteModel, + AdWriteExtraModel, AdWriteModel, } from '@modules/ad/infrastructure/ad.repository'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; @@ -147,8 +147,7 @@ describe('Ad Mapper', () => { }); it('should map domain entity to unsupported db persistence data', async () => { - const mapped: AdUnsupportedWriteModel = - adMapper.toUnsupportedPersistence(adEntity); + const mapped: AdWriteExtraModel = adMapper.toPersistenceExtra(adEntity); expect(mapped.waypoints).toBe( "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'", ); diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 29f7e2c..c843e08 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -48,7 +48,7 @@ const createAdProps: CreateAdProps = { }; const mockAdRepository = { - insertWithUnsupportedFields: jest + insertExtra: jest .fn() .mockImplementationOnce(() => ({})) .mockImplementationOnce(() => { diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index cc35ac0..5a17a2c 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -42,7 +42,7 @@ const matchQuery = new MatchQuery({ }); const mockMatcherRepository: AdRepositoryPort = { - insertWithUnsupportedFields: jest.fn(), + insertExtra: jest.fn(), findOneById: jest.fn(), findOne: jest.fn(), insert: jest.fn(), @@ -51,7 +51,6 @@ const mockMatcherRepository: AdRepositoryPort = { delete: jest.fn(), count: jest.fn(), healthCheck: jest.fn(), - queryRawUnsafe: jest.fn(), getCandidates: jest.fn().mockImplementation(() => [ { ad: { diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index 789b547..1ba6e92 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -1,13 +1,18 @@ import { AD_DIRECTION_ENCODER, + AD_MESSAGE_PUBLISHER, AD_ROUTE_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; -import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { + AdReadModel, + AdRepository, +} from '@modules/ad/infrastructure/ad.repository'; import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; -import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; +import { EventEmitterModule } from '@nestjs/event-emitter'; import { Test, TestingModule } from '@nestjs/testing'; const mockMessagePublisher = { @@ -23,17 +28,146 @@ const mockRouteProvider: RouteProviderPort = { getBasic: jest.fn(), }; +const mockPrismaService = { + $queryRawUnsafe: jest + .fn() + .mockImplementationOnce(() => { + return [ + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: new Date('2023-06-21'), + toDate: new Date('2023-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 3, + time: new Date('2023-06-21T07:05:00Z'), + margin: 900, + }, + { + uuid: '84af18ff-8779-4cac-9651-1ed5ab0713c4', + driver: true, + passenger: false, + frequency: Frequency.PUNCTUAL, + fromDate: new Date('2023-06-21'), + toDate: new Date('2023-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 349000, + driverDuration: 14300, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-18T14:16:10Z'), + updatedAt: new Date('2023-06-18T14:16:10Z'), + waypoints: 'LINESTRING(6.1765109 48.689455,2.3598 48.8589)', + day: 3, + time: new Date('2023-06-21T07:14:00Z'), + margin: 900, + }, + ]; + }) + .mockImplementationOnce(() => { + return [ + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.RECURRENT, + fromDate: new Date('2023-06-21'), + toDate: new Date('2024-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 3, + time: new Date('2023-06-21T07:05:00Z'), + margin: 900, + }, + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.RECURRENT, + fromDate: new Date('2023-06-21'), + toDate: new Date('2024-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 4, + time: new Date('2023-06-21T07:15:00Z'), + margin: 900, + }, + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.RECURRENT, + fromDate: new Date('2023-06-21'), + toDate: new Date('2024-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + day: 5, + time: new Date('2023-06-21T07:16:00Z'), + margin: 900, + }, + ]; + }) + .mockImplementationOnce(() => { + return []; + }), +}; + describe('Ad repository', () => { - let prismaService: PrismaService; - let adMapper: AdMapper; - let eventEmitter: EventEmitter2; + let adRepository: AdRepository; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [EventEmitterModule.forRoot()], providers: [ - PrismaService, AdMapper, + AdRepository, { provide: AD_DIRECTION_ENCODER, useValue: mockDirectionEncoder, @@ -42,21 +176,42 @@ describe('Ad repository', () => { provide: AD_ROUTE_PROVIDER, useValue: mockRouteProvider, }, + { + provide: AD_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + { + provide: PrismaService, + useValue: mockPrismaService, + }, ], }).compile(); - prismaService = module.get(PrismaService); - adMapper = module.get(AdMapper); - eventEmitter = module.get(EventEmitter2); + adRepository = module.get(AdRepository); }); it('should be defined', () => { - expect( - new AdRepository( - prismaService, - adMapper, - eventEmitter, - mockMessagePublisher, - ), - ).toBeDefined(); + expect(adRepository).toBeDefined(); + }); + + it('should get candidates if query returns punctual Ads', async () => { + const candidates: AdReadModel[] = await adRepository.getCandidates( + 'somePunctualQueryString', + ); + expect(candidates.length).toBe(2); + }); + + it('should get candidates if query returns recurrent Ads', async () => { + const candidates: AdReadModel[] = await adRepository.getCandidates( + 'someRecurrentQueryString', + ); + expect(candidates.length).toBe(1); + expect(candidates[0].schedule.length).toBe(3); + }); + + it('should return an empty array of candidates if query does not return Ads', async () => { + const candidates: AdReadModel[] = await adRepository.getCandidates( + 'someQueryString', + ); + expect(candidates.length).toBe(0); }); }); From 3b7b4993cfea5d2b1b378138562e4d50a3e754df Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 4 Sep 2023 11:14:21 +0200 Subject: [PATCH 11/52] add algorithm elements tests --- .../selector/passenger-oriented.selector.ts | 18 +++- .../passenger-oriented-geo-filter.spec.ts | 68 ++++++++++++++ .../core/passenger-oriented-selector.spec.ts | 94 +++++++++++++++++++ ...enger-oriented-waypoints-completer.spec.ts | 68 ++++++++++++++ 4 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts create mode 100644 src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts create mode 100644 src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 86ae892..f3d202b 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -19,13 +19,16 @@ export class PassengerOrientedSelector extends Selector { return ( await Promise.all( - queryStringRoles.map(async (queryStringRole: QueryStringRole) => ({ - ads: await this.repository.getCandidates(queryStringRole.query), - role: queryStringRole.role, - })), + queryStringRoles.map>( + async (queryStringRole: QueryStringRole) => + { + ads: await this.repository.getCandidates(queryStringRole.query), + role: queryStringRole.role, + }, + ), ) ) - .map((adsRole) => + .map((adsRole: AdsRole) => adsRole.ads.map( (adReadModel: AdReadModel) => { @@ -68,3 +71,8 @@ export type QueryStringRole = { query: string; role: Role; }; + +type AdsRole = { + ads: AdReadModel[]; + role: Role; +}; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts new file mode 100644 index 0000000..6d58d08 --- /dev/null +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -0,0 +1,68 @@ +import { PassengerOrientedGeoFilter } from '@modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { + AlgorithmType, + Candidate, +} from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery({ + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], +}); + +const candidates: Candidate[] = [ + { + ad: { + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + }, + role: Role.DRIVER, + }, + { + ad: { + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + }, + role: Role.PASSENGER, + }, +]; + +describe('Passenger oriented geo filter', () => { + it('should filter candidates', async () => { + const passengerOrientedGeoFilter: PassengerOrientedGeoFilter = + new PassengerOrientedGeoFilter(matchQuery); + const filteredCandidates: Candidate[] = + await passengerOrientedGeoFilter.filter(candidates); + expect(filteredCandidates.length).toBe(2); + }); +}); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts new file mode 100644 index 0000000..3c187f3 --- /dev/null +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -0,0 +1,94 @@ +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector'; +import { + AlgorithmType, + Candidate, +} from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery({ + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], +}); + +const mockMatcherRepository: AdRepositoryPort = { + insertExtra: jest.fn(), + findOneById: jest.fn(), + findOne: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + updateWhere: jest.fn(), + delete: jest.fn(), + count: jest.fn(), + healthCheck: jest.fn(), + getCandidates: jest.fn().mockImplementation(() => [ + { + uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: new Date('2023-06-21'), + toDate: new Date('2023-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + schedule: [ + { + day: 3, + time: new Date('2023-06-21T07:05:00Z'), + margin: 900, + }, + ], + }, + ]), +}; + +describe('Passenger oriented selector', () => { + it('should select candidates', async () => { + const passengerOrientedSelector: PassengerOrientedSelector = + new PassengerOrientedSelector(matchQuery, mockMatcherRepository); + const candidates: Candidate[] = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(2); + }); +}); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts new file mode 100644 index 0000000..097c89f --- /dev/null +++ b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts @@ -0,0 +1,68 @@ +import { PassengerOrientedWaypointsCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { + AlgorithmType, + Candidate, +} from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery({ + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], +}); + +const candidates: Candidate[] = [ + { + ad: { + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + }, + role: Role.DRIVER, + }, + { + ad: { + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + }, + role: Role.PASSENGER, + }, +]; + +describe('Passenger oriented waypoints completer', () => { + it('should complete candidates', async () => { + const passengerOrientedWaypointsCompleter: PassengerOrientedWaypointsCompleter = + new PassengerOrientedWaypointsCompleter(matchQuery); + const completedCandidates: Candidate[] = + await passengerOrientedWaypointsCompleter.complete(candidates); + expect(completedCandidates.length).toBe(2); + }); +}); From f0440ed65f9eebc9273fa5ba4c74c620fc8ea779 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 4 Sep 2023 11:46:50 +0200 Subject: [PATCH 12/52] use dddlibrary 1.3.0 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32000a1..ed5a5e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", "@mobicoop/configuration-module": "^1.2.0", - "@mobicoop/ddd-library": "file:../../packages/dddlibrary", + "@mobicoop/ddd-library": "^1.3.0", "@mobicoop/health-module": "^2.0.0", "@mobicoop/message-broker-module": "^1.2.0", "@nestjs/axios": "^2.0.0", @@ -1505,9 +1505,9 @@ } }, "node_modules/@mobicoop/ddd-library": { - "version": "1.1.1", - "resolved": "file:../../packages/dddlibrary", - "license": "AGPL", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.3.0.tgz", + "integrity": "sha512-WQTOIzGvsoh3o43Kukb9NIbJw18lsfSqu3k3cMZxc2mmgaYD7MtS4Yif/+KayQ6Ea4Ve3Hc6BVDls2X6svsoOg==", "dependencies": { "@nestjs/event-emitter": "^1.4.2", "@nestjs/microservices": "^9.4.0", diff --git a/package.json b/package.json index ab45121..8fdf7af 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", "@mobicoop/configuration-module": "^1.2.0", - "@mobicoop/ddd-library": "file:../../packages/dddlibrary", + "@mobicoop/ddd-library": "^1.3.0", "@mobicoop/health-module": "^2.0.0", "@mobicoop/message-broker-module": "^1.2.0", "@nestjs/axios": "^2.0.0", From e0030aba73f838f7df59d5d878926b46cd357150 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 4 Sep 2023 17:29:16 +0200 Subject: [PATCH 13/52] improve selector --- .../queries/match/match.query-handler.ts | 5 ++ .../application/queries/match/match.query.ts | 17 +++- .../selector/passenger-oriented.selector.ts | 78 ++++++++++++++----- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 86a3b17..14a8af5 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -7,6 +7,7 @@ import { Inject } from '@nestjs/common'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; import { AD_REPOSITORY, + AD_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; @@ -14,6 +15,7 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port'; import { DefaultParams } from '../../ports/default-params.type'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; +import { RouteProviderPort } from '../../ports/route-provider.port'; @QueryHandler(MatchQuery) export class MatchQueryHandler implements IQueryHandler { @@ -25,6 +27,8 @@ export class MatchQueryHandler implements IQueryHandler { @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, + @Inject(AD_ROUTE_PROVIDER) + private readonly routeProvider: RouteProviderPort, ) { this._defaultParams = defaultParamsProvider.getParams(); } @@ -50,6 +54,7 @@ export class MatchQueryHandler implements IQueryHandler { maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, }) .setDatesAndSchedule(this.datetimeTransformer); + await query.setRoutes(this.routeProvider); let algorithm: Algorithm; switch (query.algorithmType) { diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index b79e613..d899c1b 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -1,9 +1,11 @@ import { QueryBase } from '@mobicoop/ddd-library'; import { AlgorithmType } from '../../types/algorithm.types'; import { Waypoint } from '../../types/waypoint.type'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; +import { Route } from '../../types/route.type'; +import { RouteProviderPort } from '../../ports/route-provider.port'; export class MatchQuery extends QueryBase { driver?: boolean; @@ -26,6 +28,7 @@ export class MatchQuery extends QueryBase { maxDetourDurationRatio?: number; readonly page?: number; readonly perPage?: number; + route?: Route; constructor(props: MatchRequestDto) { super(); @@ -160,6 +163,18 @@ export class MatchQuery extends QueryBase { })); return this; }; + + setRoutes = async (routeProvider: RouteProviderPort): Promise => { + const roles: Role[] = []; + if (this.driver) roles.push(Role.DRIVER); + if (this.passenger) roles.push(Role.PASSENGER); + try { + this.route = await routeProvider.getBasic(roles, this.waypoints); + } catch (e: any) { + throw new Error('Unable to find a route for given waypoints'); + } + return this; + }; } type ScheduleItem = { diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index f3d202b..936c073 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -1,4 +1,4 @@ -import { Role } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Candidate } from '../../../types/algorithm.types'; import { Selector } from '../algorithm.abstract'; import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; @@ -8,15 +8,16 @@ export class PassengerOrientedSelector extends Selector { const queryStringRoles: QueryStringRole[] = []; if (this.query.driver) queryStringRoles.push({ - query: this.asDriverQueryString(), + query: this.createQueryString(Role.DRIVER), role: Role.DRIVER, }); if (this.query.passenger) queryStringRoles.push({ - query: this.asPassengerQueryString(), + query: this.createQueryString(Role.PASSENGER), role: Role.PASSENGER, }); + console.log(queryStringRoles); return ( await Promise.all( queryStringRoles.map>( @@ -42,29 +43,66 @@ export class PassengerOrientedSelector extends Selector { .flat(); }; - private asPassengerQueryString = (): string => `SELECT - ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, - "fromDate","toDate", - "seatsProposed","seatsRequested", - strict, - "driverDuration","driverDistance", - "passengerDuration","passengerDistance", - "fwdAzimuth","backAzimuth", - si.day,si.time,si.margin - FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" - WHERE driver=True`; + private createQueryString = (role: Role): string => + [ + this.createSelect(role), + this.createFrom(), + 'WHERE', + this.createWhere(role), + ].join(' '); - private asDriverQueryString = (): string => `SELECT + private createSelect = (role: Role): string => + [ + `SELECT ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, "fromDate","toDate", "seatsProposed","seatsRequested", strict, - "driverDuration","driverDistance", - "passengerDuration","passengerDistance", "fwdAzimuth","backAzimuth", - si.day,si.time,si.margin - FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" - WHERE passenger=True`; + si.day,si.time,si.margin`, + role == Role.DRIVER ? this.selectAsDriver() : this.selectAsPassenger(), + ].join(); + + private selectAsDriver = (): string => + `${this.query.route?.driverDuration} as duration,${this.query.route?.driverDistance} as distance`; + + private selectAsPassenger = (): string => + `"driverDuration" as duration,"driverDistance" as distance`; + + private createFrom = (): string => + 'FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid"'; + + private createWhere = (role: Role): string => + [this.whereRole(role), this.whereStrict(), this.whereDate()].join(' AND '); + + private whereRole = (role: Role): string => + role == Role.PASSENGER ? 'driver=True' : 'passenger=True'; + + private whereStrict = (): string => + this.query.strict + ? this.query.frequency == Frequency.PUNCTUAL + ? `frequency='${Frequency.PUNCTUAL}'` + : `frequency='${Frequency.RECURRENT}'` + : ''; + + private whereDate = (): string => { + const whereDate = `( + ( + "fromDate" <= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and + "toDate" >= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' + ) OR ( + "fromDate" >= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and + "toDate" <= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' + ) OR ( + "fromDate" <= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and + "toDate" <= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' + ) OR ( + "fromDate" >= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and + "toDate" >= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' + ) + )`; + return whereDate; + }; } export type QueryStringRole = { From 98530af14a9a18db35945fede4f643576748c208 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 6 Sep 2023 15:17:51 +0200 Subject: [PATCH 14/52] matcher with only db selection --- .../queries/match/match.query-handler.ts | 2 +- .../application/queries/match/match.query.ts | 9 +- .../selector/passenger-oriented.selector.ts | 265 +++++++++++++++--- .../ad/infrastructure/ad.repository.ts | 6 +- .../ad/infrastructure/time-converter.ts | 2 +- .../ad/interface/dtos/match.response.dto.ts | 4 +- .../grpc-controllers/match.grpc-controller.ts | 16 +- .../interface/grpc-controllers/matcher.proto | 1 + .../unit/core/match.query-handler.spec.ts | 10 + .../ad/tests/unit/core/match.query.spec.ts | 54 ++++ .../core/passenger-oriented-selector.spec.ts | 32 ++- .../infrastructure/time-converter.spec.ts | 1 + .../interface/match.grpc.controller.spec.ts | 13 +- 13 files changed, 354 insertions(+), 61 deletions(-) diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 14a8af5..33f9ac9 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -54,7 +54,7 @@ export class MatchQueryHandler implements IQueryHandler { maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, }) .setDatesAndSchedule(this.datetimeTransformer); - await query.setRoutes(this.routeProvider); + await query.setRoute(this.routeProvider); let algorithm: Algorithm; switch (query.algorithmType) { diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index d899c1b..a1fb409 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -112,9 +112,10 @@ export class MatchQuery extends QueryBase { setDatesAndSchedule = ( datetimeTransformer: DateTimeTransformerPort, ): MatchQuery => { + const initialFromDate: string = this.fromDate; this.fromDate = datetimeTransformer.fromDate( { - date: this.fromDate, + date: initialFromDate, time: this.schedule[0].time, coordinates: { lon: this.waypoints[0].lon, @@ -126,7 +127,7 @@ export class MatchQuery extends QueryBase { this.toDate = datetimeTransformer.toDate( this.toDate, { - date: this.fromDate, + date: initialFromDate, time: this.schedule[0].time, coordinates: { lon: this.waypoints[0].lon, @@ -164,7 +165,7 @@ export class MatchQuery extends QueryBase { return this; }; - setRoutes = async (routeProvider: RouteProviderPort): Promise => { + setRoute = async (routeProvider: RouteProviderPort): Promise => { const roles: Role[] = []; if (this.driver) roles.push(Role.DRIVER); if (this.passenger) roles.push(Role.PASSENGER); @@ -177,7 +178,7 @@ export class MatchQuery extends QueryBase { }; } -type ScheduleItem = { +export type ScheduleItem = { day?: number; time: string; margin?: number; diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 936c073..99e99f8 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -2,22 +2,24 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Candidate } from '../../../types/algorithm.types'; import { Selector } from '../algorithm.abstract'; import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; +import { ScheduleItem } from '../match.query'; +import { Waypoint } from '../../../types/waypoint.type'; +import { Coordinates } from '../../../types/coordinates.type'; export class PassengerOrientedSelector extends Selector { select = async (): Promise => { const queryStringRoles: QueryStringRole[] = []; if (this.query.driver) queryStringRoles.push({ - query: this.createQueryString(Role.DRIVER), + query: this._createQueryString(Role.DRIVER), role: Role.DRIVER, }); if (this.query.passenger) queryStringRoles.push({ - query: this.createQueryString(Role.PASSENGER), + query: this._createQueryString(Role.PASSENGER), role: Role.PASSENGER, }); - console.log(queryStringRoles); return ( await Promise.all( queryStringRoles.map>( @@ -43,66 +45,251 @@ export class PassengerOrientedSelector extends Selector { .flat(); }; - private createQueryString = (role: Role): string => + private _createQueryString = (role: Role): string => [ - this.createSelect(role), - this.createFrom(), + this._createSelect(role), + this._createFrom(), 'WHERE', - this.createWhere(role), - ].join(' '); + this._createWhere(role), + ] + .join(' ') + .replace(/\s+/g, ' '); // remove duplicate spaces for easy debug ! - private createSelect = (role: Role): string => + private _createSelect = (role: Role): string => [ - `SELECT - ad.uuid,driver,passenger,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, - "fromDate","toDate", - "seatsProposed","seatsRequested", - strict, - "fwdAzimuth","backAzimuth", + `SELECT \ + ad.uuid,driver,passenger,frequency,public.st_astext(ad.waypoints) as waypoints,\ + "fromDate","toDate",\ + "seatsProposed","seatsRequested",\ + strict,\ + "fwdAzimuth","backAzimuth",\ si.day,si.time,si.margin`, - role == Role.DRIVER ? this.selectAsDriver() : this.selectAsPassenger(), + role == Role.DRIVER ? this._selectAsDriver() : this._selectAsPassenger(), ].join(); - private selectAsDriver = (): string => + private _selectAsDriver = (): string => `${this.query.route?.driverDuration} as duration,${this.query.route?.driverDistance} as distance`; - private selectAsPassenger = (): string => + private _selectAsPassenger = (): string => `"driverDuration" as duration,"driverDistance" as distance`; - private createFrom = (): string => + private _createFrom = (): string => 'FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid"'; - private createWhere = (role: Role): string => - [this.whereRole(role), this.whereStrict(), this.whereDate()].join(' AND '); + private _createWhere = (role: Role): string => + [ + this._whereRole(role), + this._whereStrict(), + this._whereDate(), + this._whereSchedule(role), + this._whereAzimuth(), + this._whereProportion(role), + this._whereRemoteness(role), + ] + .filter((where: string) => where != '') + .join(' AND '); - private whereRole = (role: Role): string => + private _whereRole = (role: Role): string => role == Role.PASSENGER ? 'driver=True' : 'passenger=True'; - private whereStrict = (): string => + private _whereStrict = (): string => this.query.strict ? this.query.frequency == Frequency.PUNCTUAL ? `frequency='${Frequency.PUNCTUAL}'` : `frequency='${Frequency.RECURRENT}'` : ''; - private whereDate = (): string => { - const whereDate = `( - ( - "fromDate" <= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and - "toDate" >= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' - ) OR ( - "fromDate" >= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and - "toDate" <= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' - ) OR ( - "fromDate" <= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and - "toDate" <= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' - ) OR ( - "fromDate" >= '${this.query.fromDate}' and "fromDate" <= '${this.query.toDate}' and - "toDate" >= '${this.query.toDate}' and "toDate" >= '${this.query.fromDate}' - ) + private _whereDate = (): string => + `(\ + (\ + "fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ + "toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ + ) OR (\ + "fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ + "toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ + ) OR (\ + "fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ + "toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ + ) OR (\ + "fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ + "toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ + )\ )`; - return whereDate; + + private _whereSchedule = (role: Role): string => { + const schedule: string[] = []; + // we need full dates to compare times, because margins can lead to compare on previous or next day + // -first we establish a base calendar (up to a week) + const scheduleDates: Date[] = this._datesBetweenBoundaries( + this.query.fromDate, + this.query.toDate, + ); + // - then we compare each resulting day of the schedule with each day of calendar, + // adding / removing margin depending on the role + scheduleDates.map((date: Date) => { + this.query.schedule + .filter( + (scheduleItem: ScheduleItem) => date.getDay() == scheduleItem.day, + ) + .map((scheduleItem: ScheduleItem) => { + switch (role) { + case Role.PASSENGER: + schedule.push(this._wherePassengerSchedule(date, scheduleItem)); + break; + case Role.DRIVER: + schedule.push(this._whereDriverSchedule(date, scheduleItem)); + break; + } + }); + }); + if (schedule.length > 0) { + return ['(', schedule.join(' OR '), ')'].join(''); + } + return ''; }; + + private _wherePassengerSchedule = ( + date: Date, + scheduleItem: ScheduleItem, + ): string => { + let maxDepartureDatetime: Date = new Date(date); + maxDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0])); + maxDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1])); + maxDepartureDatetime = this._addMargin( + maxDepartureDatetime, + scheduleItem.margin as number, + ); + // we want the min departure time of the driver to be before the max departure time of the passenger + return `make_timestamp(\ + ${maxDepartureDatetime.getFullYear()},\ + ${maxDepartureDatetime.getMonth() + 1},\ + ${maxDepartureDatetime.getDate()},\ + CAST(EXTRACT(hour from time) as integer),\ + CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\ + make_timestamp(\ + ${maxDepartureDatetime.getFullYear()},\ + ${maxDepartureDatetime.getMonth() + 1},\ + ${maxDepartureDatetime.getDate()},${maxDepartureDatetime.getHours()},${maxDepartureDatetime.getMinutes()},0)`; + }; + + private _whereDriverSchedule = ( + date: Date, + scheduleItem: ScheduleItem, + ): string => { + let minDepartureDatetime: Date = new Date(date); + minDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0])); + minDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1])); + minDepartureDatetime = this._addMargin( + minDepartureDatetime, + -(scheduleItem.margin as number), + ); + // we want the max departure time of the passenger to be after the min departure time of the driver + return `make_timestamp(\ + ${minDepartureDatetime.getFullYear()}, + ${minDepartureDatetime.getMonth() + 1}, + ${minDepartureDatetime.getDate()},\ + CAST(EXTRACT(hour from time) as integer),\ + CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\ + make_timestamp(\ + ${minDepartureDatetime.getFullYear()}, + ${minDepartureDatetime.getMonth() + 1}, + ${minDepartureDatetime.getDate()},${minDepartureDatetime.getHours()},${minDepartureDatetime.getMinutes()},0)`; + }; + + private _whereAzimuth = (): string => { + if (!this.query.useAzimuth) return ''; + const { minAzimuth, maxAzimuth } = this._azimuthRange( + this.query.route?.backAzimuth as number, + this.query.azimuthMargin as number, + ); + if (minAzimuth <= maxAzimuth) + return `("fwdAzimuth" <= ${minAzimuth} OR "fwdAzimuth" >= ${maxAzimuth})`; + return `("fwdAzimuth" <= ${minAzimuth} AND "fwdAzimuth" >= ${maxAzimuth})`; + }; + + private _whereProportion = (role: Role): string => { + if (!this.query.useProportion) return ''; + switch (role) { + case Role.PASSENGER: + return `(${this.query.route?.passengerDistance}>(${this.query.proportion}*"driverDistance"))`; + case Role.DRIVER: + return `("passengerDistance">(${this.query.proportion}*${this.query.route?.driverDistance}))`; + } + }; + + private _whereRemoteness = (role: Role): string => { + this.query.waypoints.sort( + (firstWaypoint: Waypoint, secondWaypoint: Waypoint) => + firstWaypoint.position - secondWaypoint.position, + ); + switch (role) { + case Role.PASSENGER: + return `\ + public.st_distance('POINT(${this.query.waypoints[0].lon} ${ + this.query.waypoints[0].lat + })'::public.geography,direction)<\ + ${this.query.remoteness} AND \ + public.st_distance('POINT(${ + this.query.waypoints[this.query.waypoints.length - 1].lon + } ${ + this.query.waypoints[this.query.waypoints.length - 1].lat + })'::public.geography,direction)<\ + ${this.query.remoteness}`; + case Role.DRIVER: + const lineStringPoints: string[] = []; + this.query.route?.points.forEach((point: Coordinates) => + lineStringPoints.push( + `public.st_makepoint(${point.lon},${point.lat})`, + ), + ); + const lineString = [ + 'public.st_makeline( ARRAY[ ', + lineStringPoints.join(','), + '] )::public.geography', + ].join(''); + return `\ + public.st_distance( public.st_startpoint(waypoints::public.geometry), ${lineString})<\ + ${this.query.remoteness} AND \ + public.st_distance( public.st_endpoint(waypoints::public.geometry), ${lineString})<\ + ${this.query.remoteness}`; + } + }; + + private _datesBetweenBoundaries = ( + firstDate: string, + lastDate: string, + max = 7, + ): Date[] => { + const fromDate: Date = new Date(firstDate); + const toDate: Date = new Date(lastDate); + const dates: Date[] = []; + let count = 0; + for ( + let date = fromDate; + date <= toDate; + date.setDate(date.getDate() + 1) + ) { + dates.push(new Date(date)); + count++; + if (count == max) break; + } + return dates; + }; + + private _addMargin = (date: Date, marginInSeconds: number): Date => { + date.setTime(date.getTime() + marginInSeconds * 1000); + return date; + }; + + private _azimuthRange = ( + azimuth: number, + margin: number, + ): { minAzimuth: number; maxAzimuth: number } => ({ + minAzimuth: + azimuth - margin < 0 ? azimuth - margin + 360 : azimuth - margin, + maxAzimuth: + azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin, + }); } export type QueryStringRole = { diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index e29cde9..c7fd343 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -100,10 +100,12 @@ export class AdRepository ); } - getCandidates = async (queryString: string): Promise => - this.toAdReadModels( + getCandidates = async (queryString: string): Promise => { + // console.log(queryString); + return this.toAdReadModels( (await this.prismaRaw.$queryRawUnsafe(queryString)) as UngroupedAdModel[], ); + }; private toAdReadModels = ( ungroupedAds: UngroupedAdModel[], diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts index c3b955f..bb186de 100644 --- a/src/modules/ad/infrastructure/time-converter.ts +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -20,7 +20,7 @@ export class TimeConverter implements TimeConverterPort { date: string, time: string, timezone: string, - dst = true, + dst = false, ): Date => new Date( new DateTime( diff --git a/src/modules/ad/interface/dtos/match.response.dto.ts b/src/modules/ad/interface/dtos/match.response.dto.ts index a69fc89..bc01e6f 100644 --- a/src/modules/ad/interface/dtos/match.response.dto.ts +++ b/src/modules/ad/interface/dtos/match.response.dto.ts @@ -1,3 +1,5 @@ -export class MatchResponseDto { +import { ResponseBase } from '@mobicoop/ddd-library'; + +export class MatchResponseDto extends ResponseBase { adId: string; } diff --git a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index 7d84ec5..826b5f5 100644 --- a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -1,11 +1,12 @@ import { Controller, UsePipes } from '@nestjs/common'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { RpcValidationPipe } from '@mobicoop/ddd-library'; +import { ResponseBase, RpcValidationPipe } from '@mobicoop/ddd-library'; import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; import { QueryBus } from '@nestjs/cqrs'; import { MatchRequestDto } from './dtos/match.request.dto'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; @UsePipes( new RpcValidationPipe({ @@ -20,13 +21,18 @@ export class MatchGrpcController { @GrpcMethod('MatcherService', 'Match') async match(data: MatchRequestDto): Promise { try { - const matches = await this.queryBus.execute(new MatchQuery(data)); - return { - data: matches, + const matches: MatchEntity[] = await this.queryBus.execute( + new MatchQuery(data), + ); + return new MatchPaginatedResponseDto({ + data: matches.map((match: MatchEntity) => ({ + ...new ResponseBase(match), + adId: match.getProps().adId, + })), page: 1, perPage: 5, total: matches.length, - }; + }); } catch (e) { throw new RpcException({ code: RpcExceptionCode.UNKNOWN, diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index ceeacd3..11d9a4d 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -55,6 +55,7 @@ enum AlgorithmType { message Match { string id = 1; + string adId = 2; } message Matches { diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 801c3a1..8a717e8 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,10 +1,12 @@ import { AD_REPOSITORY, + AD_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; @@ -72,6 +74,10 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = { time: jest.fn(), }; +const mockRouteProvider: RouteProviderPort = { + getBasic: jest.fn(), +}; + describe('Match Query Handler', () => { let matchQueryHandler: MatchQueryHandler; @@ -91,6 +97,10 @@ describe('Match Query Handler', () => { provide: INPUT_DATETIME_TRANSFORMER, useValue: mockInputDateTimeTransformer, }, + { + provide: AD_ROUTE_PROVIDER, + useValue: mockRouteProvider, + }, ], }).compile(); diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts index aea2933..651227b 100644 --- a/src/modules/ad/tests/unit/core/match.query.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -1,5 +1,6 @@ import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; @@ -49,6 +50,23 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = { time: jest.fn().mockImplementation(() => '23:05'), }; +const mockRouteProvider: RouteProviderPort = { + getBasic: jest + .fn() + .mockImplementationOnce(() => ({ + driverDistance: undefined, + driverDuration: undefined, + passengerDistance: 150120, + passengerDuration: 6540, + fwdAzimuth: 276, + backAzimuth: 96, + points: [], + })) + .mockImplementationOnce(() => { + throw new Error(); + }), +}; + describe('Match Query', () => { it('should set default values', async () => { const matchQuery = new MatchQuery({ @@ -124,4 +142,40 @@ describe('Match Query', () => { expect(matchQuery.seatsProposed).toBe(3); expect(matchQuery.seatsRequested).toBe(1); }); + + it('should set route', async () => { + const matchQuery = new MatchQuery({ + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }); + await matchQuery.setRoute(mockRouteProvider); + expect(matchQuery.route?.driverDistance).toBeUndefined(); + expect(matchQuery.route?.passengerDistance).toBe(150120); + }); + + it('should throw an exception if route is not found', async () => { + const matchQuery = new MatchQuery({ + driver: true, + passenger: false, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }); + await expect(matchQuery.setRoute(mockRouteProvider)).rejects.toBeInstanceOf( + Error, + ); + }); }); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index 3c187f3..bd290a3 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -32,16 +32,44 @@ const matchQuery = new MatchQuery({ driver: true, passenger: true, frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', + fromDate: '2023-06-21', + toDate: '2023-06-21', + useAzimuth: true, + azimuthMargin: 10, + useProportion: true, + proportion: 0.3, schedule: [ { + day: 3, time: '07:05', + margin: 900, }, ], strict: false, waypoints: [originWaypoint, destinationWaypoint], }); +matchQuery.route = { + driverDistance: 150120, + driverDuration: 6540, + passengerDistance: 150120, + passengerDuration: 6540, + fwdAzimuth: 276, + backAzimuth: 96, + points: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.7566, + lon: 4.3522, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], +}; const mockMatcherRepository: AdRepositoryPort = { insertExtra: jest.fn(), diff --git a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts index df8463a..ce8ca75 100644 --- a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts @@ -60,6 +60,7 @@ describe('Time Converter', () => { parisDate, parisTime, 'Europe/Paris', + true, ); expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z'); }); diff --git a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 23ac483..91020a9 100644 --- a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -1,6 +1,7 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller'; @@ -49,12 +50,12 @@ const mockQueryBus = { execute: jest .fn() .mockImplementationOnce(() => [ - { - adId: 1, - }, - { - adId: 2, - }, + MatchEntity.create({ + adId: '0cc87f3b-7a27-4eff-9850-a5d642c2a0c3', + }), + MatchEntity.create({ + adId: 'e4cc156f-aaa5-4270-bf6f-82f5a230d748', + }), ]) .mockImplementationOnce(() => { throw new Error(); From d16997a84fbaf5169c43bc17b158020157514b52 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 6 Sep 2023 16:39:44 +0200 Subject: [PATCH 15/52] rename CarpoolRoute --- src/modules/ad/ad.di-tokens.ts | 2 +- src/modules/ad/ad.module.ts | 8 ++--- .../commands/create-ad/create-ad.service.ts | 29 ++++++++++--------- .../ports/carpool-route-provider.port.ts | 10 +++++++ .../application/ports/route-provider.port.ts | 10 ------- .../queries/match/match.query-handler.ts | 10 +++---- .../application/queries/match/match.query.ts | 15 ++++++---- .../selector/passenger-oriented.selector.ts | 10 +++---- .../{route.type.ts => carpool-route.type.ts} | 5 +++- ...-provider.ts => carpool-route-provider.ts} | 11 ++++--- .../tests/unit/core/create-ad.service.spec.ts | 11 ++++--- .../unit/core/match.query-handler.spec.ts | 8 ++--- .../ad/tests/unit/core/match.query.spec.ts | 16 +++++----- .../core/passenger-oriented-selector.spec.ts | 2 +- .../unit/infrastructure/ad.repository.spec.ts | 8 ++--- ...spec.ts => carpool-route-provider.spec.ts} | 21 +++++++------- .../geography/core/domain/route.entity.ts | 15 ++++------ .../infrastructure/graphhopper-georouter.ts | 12 ++++---- 18 files changed, 108 insertions(+), 95 deletions(-) create mode 100644 src/modules/ad/core/application/ports/carpool-route-provider.port.ts delete mode 100644 src/modules/ad/core/application/ports/route-provider.port.ts rename src/modules/ad/core/application/types/{route.type.ts => carpool-route.type.ts} (65%) rename src/modules/ad/infrastructure/{route-provider.ts => carpool-route-provider.ts} (63%) rename src/modules/ad/tests/unit/infrastructure/{route-provider.spec.ts => carpool-route-provider.spec.ts} (67%) diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 4a69ae2..592fcc5 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -4,7 +4,7 @@ export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER'); export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol( 'AD_GET_BASIC_ROUTE_CONTROLLER', ); -export const AD_ROUTE_PROVIDER = Symbol('AD_ROUTE_PROVIDER'); +export const AD_CARPOOL_ROUTE_PROVIDER = Symbol('AD_CARPOOL_ROUTE_PROVIDER'); export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 90610bd..a1f9eb6 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -4,7 +4,7 @@ import { AD_MESSAGE_PUBLISHER, AD_REPOSITORY, AD_DIRECTION_ENCODER, - AD_ROUTE_PROVIDER, + AD_CARPOOL_ROUTE_PROVIDER, AD_GET_BASIC_ROUTE_CONTROLLER, PARAMS_PROVIDER, TIMEZONE_FINDER, @@ -18,7 +18,7 @@ import { AdMapper } from './ad.mapper'; import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler'; import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller'; -import { RouteProvider } from './infrastructure/route-provider'; +import { CarpoolRouteProvider } from './infrastructure/carpool-route-provider'; import { GeographyModule } from '@modules/geography/geography.module'; import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller'; @@ -60,8 +60,8 @@ const adapters: Provider[] = [ useClass: PostgresDirectionEncoder, }, { - provide: AD_ROUTE_PROVIDER, - useClass: RouteProvider, + provide: AD_CARPOOL_ROUTE_PROVIDER, + useClass: CarpoolRouteProvider, }, { provide: AD_GET_BASIC_ROUTE_CONTROLLER, diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 2e5b606..fb84626 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -1,29 +1,32 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CreateAdCommand } from './create-ad.command'; import { Inject } from '@nestjs/common'; -import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; +import { + AD_REPOSITORY, + AD_CARPOOL_ROUTE_PROVIDER, +} from '@modules/ad/ad.di-tokens'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; -import { RouteProviderPort } from '../../ports/route-provider.port'; +import { CarpoolRouteProviderPort } from '../../ports/carpool-route-provider.port'; import { Role } from '@modules/ad/core/domain/ad.types'; -import { Route } from '../../types/route.type'; +import { CarpoolRoute } from '../../types/carpool-route.type'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { constructor( @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, - @Inject(AD_ROUTE_PROVIDER) - private readonly routeProvider: RouteProviderPort, + @Inject(AD_CARPOOL_ROUTE_PROVIDER) + private readonly carpoolRouteProvider: CarpoolRouteProviderPort, ) {} async execute(command: CreateAdCommand): Promise { const roles: Role[] = []; if (command.driver) roles.push(Role.DRIVER); if (command.passenger) roles.push(Role.PASSENGER); - const route: Route = await this.routeProvider.getBasic( + const carpoolRoute: CarpoolRoute = await this.carpoolRouteProvider.getBasic( roles, command.waypoints, ); @@ -39,13 +42,13 @@ export class CreateAdService implements ICommandHandler { seatsRequested: command.seatsRequested, strict: command.strict, waypoints: command.waypoints, - points: route.points, - driverDistance: route.driverDistance, - driverDuration: route.driverDuration, - passengerDistance: route.passengerDistance, - passengerDuration: route.passengerDuration, - fwdAzimuth: route.fwdAzimuth, - backAzimuth: route.backAzimuth, + points: carpoolRoute.points, + driverDistance: carpoolRoute.driverDistance, + driverDuration: carpoolRoute.driverDuration, + passengerDistance: carpoolRoute.passengerDistance, + passengerDuration: carpoolRoute.passengerDuration, + fwdAzimuth: carpoolRoute.fwdAzimuth, + backAzimuth: carpoolRoute.backAzimuth, }); try { diff --git a/src/modules/ad/core/application/ports/carpool-route-provider.port.ts b/src/modules/ad/core/application/ports/carpool-route-provider.port.ts new file mode 100644 index 0000000..24b7f5a --- /dev/null +++ b/src/modules/ad/core/application/ports/carpool-route-provider.port.ts @@ -0,0 +1,10 @@ +import { Role } from '../../domain/ad.types'; +import { Waypoint } from '../types/waypoint.type'; +import { CarpoolRoute } from '../types/carpool-route.type'; + +export interface CarpoolRouteProviderPort { + /** + * Get a basic carpool route with points and overall duration / distance + */ + getBasic(roles: Role[], waypoints: Waypoint[]): Promise; +} diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts deleted file mode 100644 index 4ce43b0..0000000 --- a/src/modules/ad/core/application/ports/route-provider.port.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Role } from '../../domain/ad.types'; -import { Waypoint } from '../types/waypoint.type'; -import { Route } from '../types/route.type'; - -export interface RouteProviderPort { - /** - * Get a basic route with points and overall duration / distance - */ - getBasic(roles: Role[], waypoints: Waypoint[]): Promise; -} diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 33f9ac9..7aa5770 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -7,7 +7,7 @@ import { Inject } from '@nestjs/common'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; import { AD_REPOSITORY, - AD_ROUTE_PROVIDER, + AD_CARPOOL_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; @@ -15,7 +15,7 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port'; import { DefaultParams } from '../../ports/default-params.type'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; -import { RouteProviderPort } from '../../ports/route-provider.port'; +import { CarpoolRouteProviderPort } from '../../ports/carpool-route-provider.port'; @QueryHandler(MatchQuery) export class MatchQueryHandler implements IQueryHandler { @@ -27,8 +27,8 @@ export class MatchQueryHandler implements IQueryHandler { @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, - @Inject(AD_ROUTE_PROVIDER) - private readonly routeProvider: RouteProviderPort, + @Inject(AD_CARPOOL_ROUTE_PROVIDER) + private readonly routeProvider: CarpoolRouteProviderPort, ) { this._defaultParams = defaultParamsProvider.getParams(); } @@ -54,7 +54,7 @@ export class MatchQueryHandler implements IQueryHandler { maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, }) .setDatesAndSchedule(this.datetimeTransformer); - await query.setRoute(this.routeProvider); + await query.setCarpoolRoute(this.routeProvider); let algorithm: Algorithm; switch (query.algorithmType) { diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index a1fb409..a329da9 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -4,8 +4,8 @@ import { Waypoint } from '../../types/waypoint.type'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; -import { Route } from '../../types/route.type'; -import { RouteProviderPort } from '../../ports/route-provider.port'; +import { CarpoolRoute } from '../../types/carpool-route.type'; +import { CarpoolRouteProviderPort } from '../../ports/carpool-route-provider.port'; export class MatchQuery extends QueryBase { driver?: boolean; @@ -28,7 +28,7 @@ export class MatchQuery extends QueryBase { maxDetourDurationRatio?: number; readonly page?: number; readonly perPage?: number; - route?: Route; + carpoolRoute?: CarpoolRoute; constructor(props: MatchRequestDto) { super(); @@ -165,12 +165,17 @@ export class MatchQuery extends QueryBase { return this; }; - setRoute = async (routeProvider: RouteProviderPort): Promise => { + setCarpoolRoute = async ( + carpoolRouteProvider: CarpoolRouteProviderPort, + ): Promise => { const roles: Role[] = []; if (this.driver) roles.push(Role.DRIVER); if (this.passenger) roles.push(Role.PASSENGER); try { - this.route = await routeProvider.getBasic(roles, this.waypoints); + this.carpoolRoute = await carpoolRouteProvider.getBasic( + roles, + this.waypoints, + ); } catch (e: any) { throw new Error('Unable to find a route for given waypoints'); } diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 99e99f8..70f3cf0 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -68,7 +68,7 @@ export class PassengerOrientedSelector extends Selector { ].join(); private _selectAsDriver = (): string => - `${this.query.route?.driverDuration} as duration,${this.query.route?.driverDistance} as distance`; + `${this.query.carpoolRoute?.driverDuration} as duration,${this.query.carpoolRoute?.driverDistance} as distance`; private _selectAsPassenger = (): string => `"driverDuration" as duration,"driverDistance" as distance`; @@ -199,7 +199,7 @@ export class PassengerOrientedSelector extends Selector { private _whereAzimuth = (): string => { if (!this.query.useAzimuth) return ''; const { minAzimuth, maxAzimuth } = this._azimuthRange( - this.query.route?.backAzimuth as number, + this.query.carpoolRoute?.backAzimuth as number, this.query.azimuthMargin as number, ); if (minAzimuth <= maxAzimuth) @@ -211,9 +211,9 @@ export class PassengerOrientedSelector extends Selector { if (!this.query.useProportion) return ''; switch (role) { case Role.PASSENGER: - return `(${this.query.route?.passengerDistance}>(${this.query.proportion}*"driverDistance"))`; + return `(${this.query.carpoolRoute?.passengerDistance}>(${this.query.proportion}*"driverDistance"))`; case Role.DRIVER: - return `("passengerDistance">(${this.query.proportion}*${this.query.route?.driverDistance}))`; + return `("passengerDistance">(${this.query.proportion}*${this.query.carpoolRoute?.driverDistance}))`; } }; @@ -237,7 +237,7 @@ export class PassengerOrientedSelector extends Selector { ${this.query.remoteness}`; case Role.DRIVER: const lineStringPoints: string[] = []; - this.query.route?.points.forEach((point: Coordinates) => + this.query.carpoolRoute?.points.forEach((point: Coordinates) => lineStringPoints.push( `public.st_makepoint(${point.lon},${point.lat})`, ), diff --git a/src/modules/ad/core/application/types/route.type.ts b/src/modules/ad/core/application/types/carpool-route.type.ts similarity index 65% rename from src/modules/ad/core/application/types/route.type.ts rename to src/modules/ad/core/application/types/carpool-route.type.ts index 971e2a9..7f07fba 100644 --- a/src/modules/ad/core/application/types/route.type.ts +++ b/src/modules/ad/core/application/types/carpool-route.type.ts @@ -1,6 +1,9 @@ import { Coordinates } from './coordinates.type'; -export type Route = { +/** + * A carpool route is a route with distance and duration as driver and / or passenger + */ +export type CarpoolRoute = { driverDistance?: number; driverDuration?: number; passengerDistance?: number; diff --git a/src/modules/ad/infrastructure/route-provider.ts b/src/modules/ad/infrastructure/carpool-route-provider.ts similarity index 63% rename from src/modules/ad/infrastructure/route-provider.ts rename to src/modules/ad/infrastructure/carpool-route-provider.ts index cd57ec2..97bd5c9 100644 --- a/src/modules/ad/infrastructure/route-provider.ts +++ b/src/modules/ad/infrastructure/carpool-route-provider.ts @@ -1,19 +1,22 @@ import { Inject, Injectable } from '@nestjs/common'; -import { RouteProviderPort } from '../core/application/ports/route-provider.port'; -import { Route } from '../core/application/types/route.type'; +import { CarpoolRouteProviderPort } from '../core/application/ports/carpool-route-provider.port'; +import { CarpoolRoute } from '../core/application/types/carpool-route.type'; import { Waypoint } from '../core/application/types/waypoint.type'; import { Role } from '../core/domain/ad.types'; import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; @Injectable() -export class RouteProvider implements RouteProviderPort { +export class CarpoolRouteProvider implements CarpoolRouteProviderPort { constructor( @Inject(AD_GET_BASIC_ROUTE_CONTROLLER) private readonly getBasicRouteController: GetBasicRouteControllerPort, ) {} - getBasic = async (roles: Role[], waypoints: Waypoint[]): Promise => + getBasic = async ( + roles: Role[], + waypoints: Waypoint[], + ): Promise => await this.getBasicRouteController.get({ roles, waypoints, diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index c843e08..9f53e98 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -1,5 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; +import { + AD_REPOSITORY, + AD_CARPOOL_ROUTE_PROVIDER, +} from '@modules/ad/ad.di-tokens'; import { AggregateID } from '@mobicoop/ddd-library'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { ConflictException } from '@mobicoop/ddd-library'; @@ -8,7 +11,7 @@ import { CreateAdService } from '@modules/ad/core/application/commands/create-ad import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; -import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; const originWaypoint: WaypointProps = { position: 0, @@ -59,7 +62,7 @@ const mockAdRepository = { }), }; -const mockRouteProvider: RouteProviderPort = { +const mockRouteProvider: CarpoolRouteProviderPort = { getBasic: jest.fn().mockImplementation(() => ({ driverDistance: 350101, driverDuration: 14422, @@ -96,7 +99,7 @@ describe('create-ad.service', () => { useValue: mockAdRepository, }, { - provide: AD_ROUTE_PROVIDER, + provide: AD_CARPOOL_ROUTE_PROVIDER, useValue: mockRouteProvider, }, CreateAdService, diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 8a717e8..25b7a8a 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,12 +1,12 @@ import { AD_REPOSITORY, - AD_ROUTE_PROVIDER, + AD_CARPOOL_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; -import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; @@ -74,7 +74,7 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = { time: jest.fn(), }; -const mockRouteProvider: RouteProviderPort = { +const mockRouteProvider: CarpoolRouteProviderPort = { getBasic: jest.fn(), }; @@ -98,7 +98,7 @@ describe('Match Query Handler', () => { useValue: mockInputDateTimeTransformer, }, { - provide: AD_ROUTE_PROVIDER, + provide: AD_CARPOOL_ROUTE_PROVIDER, useValue: mockRouteProvider, }, ], diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts index 651227b..3c8c4d5 100644 --- a/src/modules/ad/tests/unit/core/match.query.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -1,6 +1,6 @@ import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type'; -import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; @@ -50,7 +50,7 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = { time: jest.fn().mockImplementation(() => '23:05'), }; -const mockRouteProvider: RouteProviderPort = { +const mockRouteProvider: CarpoolRouteProviderPort = { getBasic: jest .fn() .mockImplementationOnce(() => ({ @@ -155,9 +155,9 @@ describe('Match Query', () => { ], waypoints: [originWaypoint, destinationWaypoint], }); - await matchQuery.setRoute(mockRouteProvider); - expect(matchQuery.route?.driverDistance).toBeUndefined(); - expect(matchQuery.route?.passengerDistance).toBe(150120); + await matchQuery.setCarpoolRoute(mockRouteProvider); + expect(matchQuery.carpoolRoute?.driverDistance).toBeUndefined(); + expect(matchQuery.carpoolRoute?.passengerDistance).toBe(150120); }); it('should throw an exception if route is not found', async () => { @@ -174,8 +174,8 @@ describe('Match Query', () => { ], waypoints: [originWaypoint, destinationWaypoint], }); - await expect(matchQuery.setRoute(mockRouteProvider)).rejects.toBeInstanceOf( - Error, - ); + await expect( + matchQuery.setCarpoolRoute(mockRouteProvider), + ).rejects.toBeInstanceOf(Error); }); }); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index bd290a3..1b9860d 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -48,7 +48,7 @@ const matchQuery = new MatchQuery({ strict: false, waypoints: [originWaypoint, destinationWaypoint], }); -matchQuery.route = { +matchQuery.carpoolRoute = { driverDistance: 150120, driverDuration: 6540, passengerDistance: 150120, diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index 1ba6e92..ce217c6 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -1,10 +1,10 @@ import { AD_DIRECTION_ENCODER, AD_MESSAGE_PUBLISHER, - AD_ROUTE_PROVIDER, + AD_CARPOOL_ROUTE_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; -import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AdReadModel, @@ -24,7 +24,7 @@ const mockDirectionEncoder: DirectionEncoderPort = { decode: jest.fn(), }; -const mockRouteProvider: RouteProviderPort = { +const mockRouteProvider: CarpoolRouteProviderPort = { getBasic: jest.fn(), }; @@ -173,7 +173,7 @@ describe('Ad repository', () => { useValue: mockDirectionEncoder, }, { - provide: AD_ROUTE_PROVIDER, + provide: AD_CARPOOL_ROUTE_PROVIDER, useValue: mockRouteProvider, }, { diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts similarity index 67% rename from src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts rename to src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts index 7a41ed8..fb35530 100644 --- a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts @@ -1,7 +1,7 @@ import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens'; -import { Route } from '@modules/ad/core/application/types/route.type'; +import { CarpoolRoute } from '@modules/ad/core/application/types/carpool-route.type'; import { Role } from '@modules/ad/core/domain/ad.types'; -import { RouteProvider } from '@modules/ad/infrastructure/route-provider'; +import { CarpoolRouteProvider } from '@modules/ad/infrastructure/carpool-route-provider'; import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; import { Test, TestingModule } from '@nestjs/testing'; @@ -27,13 +27,13 @@ const mockGetBasicRouteController: GetBasicRouteControllerPort = { })), }; -describe('Route provider', () => { - let routeProvider: RouteProvider; +describe('Carpool route provider', () => { + let carpoolRouteProvider: CarpoolRouteProvider; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - RouteProvider, + CarpoolRouteProvider, { provide: AD_GET_BASIC_ROUTE_CONTROLLER, useValue: mockGetBasicRouteController, @@ -41,15 +41,16 @@ describe('Route provider', () => { ], }).compile(); - routeProvider = module.get(RouteProvider); + carpoolRouteProvider = + module.get(CarpoolRouteProvider); }); it('should be defined', () => { - expect(routeProvider).toBeDefined(); + expect(carpoolRouteProvider).toBeDefined(); }); - it('should provide a route', async () => { - const route: Route = await routeProvider.getBasic( + it('should provide a carpool route', async () => { + const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( [Role.DRIVER], [ { @@ -64,6 +65,6 @@ describe('Route provider', () => { }, ], ); - expect(route.driverDistance).toBe(23000); + expect(carpoolRoute.driverDistance).toBe(23000); }); }); diff --git a/src/modules/geography/core/domain/route.entity.ts b/src/modules/geography/core/domain/route.entity.ts index 2930482..9bbee40 100644 --- a/src/modules/geography/core/domain/route.entity.ts +++ b/src/modules/geography/core/domain/route.entity.ts @@ -15,16 +15,11 @@ export class RouteEntity extends AggregateRoot { protected readonly _id: AggregateID; static create = async (create: CreateRouteProps): Promise => { - let routes: Route[]; - try { - routes = await create.georouter.routes( - this.getPaths(create.roles, create.waypoints), - create.georouterSettings, - ); - if (!routes || routes.length == 0) throw new RouteNotFoundException(); - } catch (e: any) { - throw e; - } + const routes: Route[] = await create.georouter.routes( + this.getPaths(create.roles, create.waypoints), + create.georouterSettings, + ); + if (!routes || routes.length == 0) throw new RouteNotFoundException(); let baseRoute: Route; let driverRoute: Route | undefined; let passengerRoute: Route | undefined; diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts index 198fcf3..37278d6 100644 --- a/src/modules/geography/infrastructure/graphhopper-georouter.ts +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -39,16 +39,16 @@ export class GraphhopperGeorouter implements GeorouterPort { paths: Path[], settings: GeorouterSettings, ): Promise => { - this.setDefaultUrlArgs(); - this.setSettings(settings); - return this.getRoutes(paths); + this._setDefaultUrlArgs(); + this._setSettings(settings); + return this._getRoutes(paths); }; - private setDefaultUrlArgs = (): void => { + private _setDefaultUrlArgs = (): void => { this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false']; }; - private setSettings = (settings: GeorouterSettings): void => { + private _setSettings = (settings: GeorouterSettings): void => { if (settings.detailedDuration) { this.urlArgs.push('details=time'); } @@ -62,7 +62,7 @@ export class GraphhopperGeorouter implements GeorouterPort { } }; - private getRoutes = async (paths: Path[]): Promise => { + private _getRoutes = async (paths: Path[]): Promise => { const routes = Promise.all( paths.map(async (path) => { const url: string = [ From 61a1d5671795c8e44aea63b430995b47af8c8081 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 6 Sep 2023 16:54:53 +0200 Subject: [PATCH 16/52] fix tests --- .../queries/match/match.query-handler.ts | 4 +- .../selector/passenger-oriented.selector.ts | 1 + .../core/application/types/algorithm.types.ts | 7 +++ .../passenger-oriented-geo-filter.spec.ts | 44 +++++++++++++++++++ ...enger-oriented-waypoints-completer.spec.ts | 44 +++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 7aa5770..7510a5f 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -28,7 +28,7 @@ export class MatchQueryHandler implements IQueryHandler { @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, @Inject(AD_CARPOOL_ROUTE_PROVIDER) - private readonly routeProvider: CarpoolRouteProviderPort, + private readonly carpoolRouteProvider: CarpoolRouteProviderPort, ) { this._defaultParams = defaultParamsProvider.getParams(); } @@ -54,7 +54,7 @@ export class MatchQueryHandler implements IQueryHandler { maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, }) .setDatesAndSchedule(this.datetimeTransformer); - await query.setCarpoolRoute(this.routeProvider); + await query.setCarpoolRoute(this.carpoolRouteProvider); let algorithm: Algorithm; switch (query.algorithmType) { diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 70f3cf0..fe9e67b 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -39,6 +39,7 @@ export class PassengerOrientedSelector extends Selector { id: adReadModel.uuid, }, role: adsRole.role, + baseCarpoolRoute: this.query.carpoolRoute, }, ), ) diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts index a3e75a8..83cc2aa 100644 --- a/src/modules/ad/core/application/types/algorithm.types.ts +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -1,12 +1,19 @@ import { Role } from '../../domain/ad.types'; +import { CarpoolRoute } from './carpool-route.type'; export enum AlgorithmType { PASSENGER_ORIENTED = 'PASSENGER_ORIENTED', } +/** + * A candidate is a potential match + */ export type Candidate = { ad: Ad; role: Role; + baseCarpoolRoute: CarpoolRoute; + // driverRoute?: Route; ? + // crewRoute?: Route; ? }; export type Ad = { diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 6d58d08..9d34a9f 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -48,12 +48,56 @@ const candidates: Candidate[] = [ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', }, role: Role.DRIVER, + baseCarpoolRoute: { + driverDistance: 350101, + driverDuration: 14422, + passengerDistance: 350101, + passengerDuration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + }, }, { ad: { id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', }, role: Role.PASSENGER, + baseCarpoolRoute: { + driverDistance: 350101, + driverDuration: 14422, + passengerDistance: 350101, + passengerDuration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + }, }, ]; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts index 097c89f..5ca81c3 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts @@ -48,12 +48,56 @@ const candidates: Candidate[] = [ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', }, role: Role.DRIVER, + baseCarpoolRoute: { + driverDistance: 350101, + driverDuration: 14422, + passengerDistance: 350101, + passengerDuration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + }, }, { ad: { id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', }, role: Role.PASSENGER, + baseCarpoolRoute: { + driverDistance: 350101, + driverDuration: 14422, + passengerDistance: 350101, + passengerDuration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + }, }, ]; From 57fe8d417f5de67ea8dc3f73b8553e1170a9ef36 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 6 Sep 2023 16:57:22 +0200 Subject: [PATCH 17/52] fix tests --- .../ad/core/application/queries/match/algorithm.abstract.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index c5df2d4..7fe7d78 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -13,7 +13,7 @@ export abstract class Algorithm { ) {} /** - * Filter candidates that matches the query + * Return Ads that matches the query as Matches */ match = async (): Promise => { this.candidates = await this.selector.select(); From d1a314f0113686b86963f0b4bfd810d950886a81 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 7 Sep 2023 14:30:07 +0200 Subject: [PATCH 18/52] extract carpool informations from geography module --- .../selector/passenger-oriented.selector.ts | 4 +- .../ad/core/application/types/address.type.ts | 4 +- .../application/types/carpool-route.type.ts | 4 +- .../{coordinates.type.ts => point.type.ts} | 2 +- .../infrastructure/carpool-route-provider.ts | 118 ++++++++- .../carpool-route-provider.spec.ts | 210 +++++++++++++-- .../ports/direction-encoder.port.ts | 6 +- .../core/application/ports/georouter.port.ts | 4 +- .../get-route/get-route.query-handler.ts | 1 - .../queries/get-route/get-route.query.ts | 10 +- .../geography/core/domain/route.entity.ts | 129 ++++----- .../geography/core/domain/route.types.ts | 40 +-- ....value-object.ts => point.value-object.ts} | 6 +- ...t.value-object.ts => step.value-object.ts} | 6 +- .../infrastructure/graphhopper-georouter.ts | 76 +++--- .../postgres-direction-encoder.ts | 6 +- .../controllers/dtos/get-route.request.dto.ts | 3 +- .../controllers/get-basic-route.controller.ts | 2 +- .../interface/dtos/route.response.dto.ts | 13 +- src/modules/geography/route.mapper.ts | 14 +- .../unit/core/get-route.query-handler.spec.ts | 7 +- ...ect.spec.ts => point.value-object.spec.ts} | 20 +- .../tests/unit/core/route.entity.spec.ts | 245 +++--------------- ...ject.spec.ts => step.value-object.spec.ts} | 28 +- .../graphhopper-georouter.spec.ts | 205 +++++++-------- .../postgres-direction-encoder.spec.ts | 19 +- .../get-basic-route.controller.spec.ts | 2 - .../geography/tests/unit/route.mapper.spec.ts | 13 +- 28 files changed, 586 insertions(+), 611 deletions(-) rename src/modules/ad/core/application/types/{coordinates.type.ts => point.type.ts} (54%) rename src/modules/geography/core/domain/value-objects/{coordinates.value-object.ts => point.value-object.ts} (79%) rename src/modules/geography/core/domain/value-objects/{spacetime-point.value-object.ts => step.value-object.ts} (86%) rename src/modules/geography/tests/unit/core/{coordinates.value-object.spec.ts => point.value-object.spec.ts} (67%) rename src/modules/geography/tests/unit/core/{spacetime-point.value-object.spec.ts => step.value-object.spec.ts} (74%) diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index fe9e67b..5c828ea 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -4,7 +4,7 @@ import { Selector } from '../algorithm.abstract'; import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; import { ScheduleItem } from '../match.query'; import { Waypoint } from '../../../types/waypoint.type'; -import { Coordinates } from '../../../types/coordinates.type'; +import { Point } from '../../../types/point.type'; export class PassengerOrientedSelector extends Selector { select = async (): Promise => { @@ -238,7 +238,7 @@ export class PassengerOrientedSelector extends Selector { ${this.query.remoteness}`; case Role.DRIVER: const lineStringPoints: string[] = []; - this.query.carpoolRoute?.points.forEach((point: Coordinates) => + this.query.carpoolRoute?.points.forEach((point: Point) => lineStringPoints.push( `public.st_makepoint(${point.lon},${point.lat})`, ), diff --git a/src/modules/ad/core/application/types/address.type.ts b/src/modules/ad/core/application/types/address.type.ts index e37174a..af02842 100644 --- a/src/modules/ad/core/application/types/address.type.ts +++ b/src/modules/ad/core/application/types/address.type.ts @@ -1,4 +1,4 @@ -import { Coordinates } from './coordinates.type'; +import { Point } from './point.type'; export type Address = { name?: string; @@ -7,4 +7,4 @@ export type Address = { locality?: string; postalCode?: string; country?: string; -} & Coordinates; +} & Point; diff --git a/src/modules/ad/core/application/types/carpool-route.type.ts b/src/modules/ad/core/application/types/carpool-route.type.ts index 7f07fba..b219701 100644 --- a/src/modules/ad/core/application/types/carpool-route.type.ts +++ b/src/modules/ad/core/application/types/carpool-route.type.ts @@ -1,4 +1,4 @@ -import { Coordinates } from './coordinates.type'; +import { Point } from './point.type'; /** * A carpool route is a route with distance and duration as driver and / or passenger @@ -10,5 +10,5 @@ export type CarpoolRoute = { passengerDuration?: number; fwdAzimuth: number; backAzimuth: number; - points: Coordinates[]; + points: Point[]; }; diff --git a/src/modules/ad/core/application/types/coordinates.type.ts b/src/modules/ad/core/application/types/point.type.ts similarity index 54% rename from src/modules/ad/core/application/types/coordinates.type.ts rename to src/modules/ad/core/application/types/point.type.ts index 8e149ed..9bb160e 100644 --- a/src/modules/ad/core/application/types/coordinates.type.ts +++ b/src/modules/ad/core/application/types/point.type.ts @@ -1,4 +1,4 @@ -export type Coordinates = { +export type Point = { lon: number; lat: number; }; diff --git a/src/modules/ad/infrastructure/carpool-route-provider.ts b/src/modules/ad/infrastructure/carpool-route-provider.ts index 97bd5c9..5cdb4dd 100644 --- a/src/modules/ad/infrastructure/carpool-route-provider.ts +++ b/src/modules/ad/infrastructure/carpool-route-provider.ts @@ -5,6 +5,7 @@ import { Waypoint } from '../core/application/types/waypoint.type'; import { Role } from '../core/domain/ad.types'; import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; +import { Route } from '@modules/geography/core/domain/route.types'; @Injectable() export class CarpoolRouteProvider implements CarpoolRouteProviderPort { @@ -16,9 +17,116 @@ export class CarpoolRouteProvider implements CarpoolRouteProviderPort { getBasic = async ( roles: Role[], waypoints: Waypoint[], - ): Promise => - await this.getBasicRouteController.get({ - roles, - waypoints, - }); + ): Promise => { + const paths: Path[] = this.getPaths(roles, waypoints); + const typeRoutes: TypeRoute[] = await Promise.all( + paths.map( + async (path: Path) => + { + type: path.type, + route: await this.getBasicRouteController.get({ + waypoints, + }), + }, + ), + ); + return this._toCarpoolRoute(typeRoutes); + }; + + private _toCarpoolRoute = (typeRoutes: TypeRoute[]): CarpoolRoute => { + let baseRoute: Route; + let driverRoute: Route | undefined; + let passengerRoute: Route | undefined; + if ( + typeRoutes.some( + (typeRoute: TypeRoute) => typeRoute.type == PathType.GENERIC, + ) + ) { + driverRoute = passengerRoute = typeRoutes.find( + (typeRoute: TypeRoute) => typeRoute.type == PathType.GENERIC, + )?.route; + } else { + driverRoute = typeRoutes.some( + (typeRoute: TypeRoute) => typeRoute.type == PathType.DRIVER, + ) + ? typeRoutes.find( + (typeRoute: TypeRoute) => typeRoute.type == PathType.DRIVER, + )?.route + : undefined; + passengerRoute = typeRoutes.some( + (typeRoute: TypeRoute) => typeRoute.type == PathType.PASSENGER, + ) + ? typeRoutes.find( + (typeRoute: TypeRoute) => typeRoute.type == PathType.PASSENGER, + )?.route + : undefined; + } + if (driverRoute) { + baseRoute = driverRoute; + } else { + baseRoute = passengerRoute as Route; + } + return { + driverDistance: driverRoute?.distance, + driverDuration: driverRoute?.duration, + passengerDistance: passengerRoute?.distance, + passengerDuration: passengerRoute?.duration, + fwdAzimuth: baseRoute.fwdAzimuth, + backAzimuth: baseRoute.backAzimuth, + points: baseRoute.points, + }; + }; + + private getPaths = (roles: Role[], waypoints: Waypoint[]): Path[] => { + const paths: Path[] = []; + if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { + if (waypoints.length == 2) { + // 2 points => same route for driver and passenger + paths.push(this.createGenericPath(waypoints)); + } else { + paths.push( + this.createDriverPath(waypoints), + this.createPassengerPath(waypoints), + ); + } + } else if (roles.includes(Role.DRIVER)) { + paths.push(this.createDriverPath(waypoints)); + } else if (roles.includes(Role.PASSENGER)) { + paths.push(this.createPassengerPath(waypoints)); + } + return paths; + }; + + private createGenericPath = (waypoints: Waypoint[]): Path => + this.createPath(waypoints, PathType.GENERIC); + + private createDriverPath = (waypoints: Waypoint[]): Path => + this.createPath(waypoints, PathType.DRIVER); + + private createPassengerPath = (waypoints: Waypoint[]): Path => + this.createPath( + [waypoints[0], waypoints[waypoints.length - 1]], + PathType.PASSENGER, + ); + + private createPath = (waypoints: Waypoint[], type: PathType): Path => ({ + type, + waypoints, + }); +} + +type Path = { + type: PathType; + waypoints: Waypoint[]; +}; + +type TypeRoute = { + type: PathType; + route: Route; +}; + +enum PathType { + GENERIC = 'generic', + DRIVER = 'driver', + PASSENGER = 'passenger', } diff --git a/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts index fb35530..3e96571 100644 --- a/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts @@ -1,30 +1,133 @@ import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens'; import { CarpoolRoute } from '@modules/ad/core/application/types/carpool-route.type'; +import { Point } from '@modules/ad/core/application/types/point.type'; import { Role } from '@modules/ad/core/domain/ad.types'; import { CarpoolRouteProvider } from '@modules/ad/infrastructure/carpool-route-provider'; import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; import { Test, TestingModule } from '@nestjs/testing'; +const originPoint: Point = { + lat: 48.689445, + lon: 6.17651, +}; +const destinationPoint: Point = { + lat: 48.8566, + lon: 2.3522, +}; +const additionalPoint: Point = { + lon: 48.7566, + lat: 4.4498, +}; + const mockGetBasicRouteController: GetBasicRouteControllerPort = { - get: jest.fn().mockImplementation(() => ({ - driverDistance: 23000, - driverDuration: 900, - passengerDistance: 23000, - passengerDuration: 900, - fwdAzimuth: 283, - backAzimuth: 93, - distanceAzimuth: 19840, - points: [ - { - lon: 6.1765103, - lat: 48.689446, - }, - { - lon: 2.3523, - lat: 48.8567, - }, - ], - })), + get: jest + .fn() + .mockImplementationOnce(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + })) + .mockImplementationOnce(() => ({ + distance: 350102, + duration: 14423, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336545, + points: [ + { + lon: 6.1765103, + lat: 48.689446, + }, + { + lon: 4.984579, + lat: 48.725688, + }, + { + lon: 2.3523, + lat: 48.8567, + }, + ], + })) + .mockImplementationOnce(() => ({ + distance: 350100, + duration: 14421, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336543, + points: [ + { + lon: 6.1765101, + lat: 48.689444, + }, + { + lon: 4.984577, + lat: 48.725686, + }, + { + lon: 2.3521, + lat: 48.8565, + }, + ], + })) + .mockImplementationOnce(() => ({ + distance: 350107, + duration: 14427, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336548, + points: [ + { + lon: 6.1765101, + lat: 48.689444, + }, + { + lon: 4.984577, + lat: 48.725686, + }, + { + lon: 2.3521, + lat: 48.8565, + }, + ], + })) + .mockImplementationOnce(() => ({ + distance: 350108, + duration: 14428, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336548, + points: [ + { + lon: 6.1765101, + lat: 48.689444, + }, + { + lon: 4.984577, + lat: 48.725686, + }, + { + lon: 2.3521, + lat: 48.8565, + }, + ], + })) + .mockImplementationOnce(() => []), }; describe('Carpool route provider', () => { @@ -49,22 +152,79 @@ describe('Carpool route provider', () => { expect(carpoolRouteProvider).toBeDefined(); }); - it('should provide a carpool route', async () => { + it('should provide a carpool route for a driver only', async () => { const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( [Role.DRIVER], [ { position: 0, - lat: 48.689445, - lon: 6.1765102, + ...originPoint, }, { position: 1, - lat: 48.8566, - lon: 2.3522, + ...destinationPoint, }, ], ); - expect(carpoolRoute.driverDistance).toBe(23000); + expect(carpoolRoute.driverDistance).toBe(350101); + expect(carpoolRoute.passengerDuration).toBeUndefined(); + }); + + it('should provide a carpool route for a passenger only', async () => { + const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( + [Role.PASSENGER], + [ + { + position: 0, + ...originPoint, + }, + { + position: 1, + ...destinationPoint, + }, + ], + ); + expect(carpoolRoute.passengerDistance).toBe(350102); + expect(carpoolRoute.driverDuration).toBeUndefined(); + }); + + it('should provide a simple carpool route for a driver and passenger', async () => { + const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( + [Role.DRIVER, Role.PASSENGER], + [ + { + position: 0, + ...originPoint, + }, + { + position: 1, + ...destinationPoint, + }, + ], + ); + expect(carpoolRoute.driverDuration).toBe(14421); + expect(carpoolRoute.passengerDistance).toBe(350100); + }); + + it('should provide a complex carpool route for a driver and passenger', async () => { + const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( + [Role.DRIVER, Role.PASSENGER], + [ + { + position: 0, + ...originPoint, + }, + { + position: 1, + ...additionalPoint, + }, + { + position: 2, + ...destinationPoint, + }, + ], + ); + expect(carpoolRoute.driverDistance).toBe(350107); + expect(carpoolRoute.passengerDuration).toBe(14428); }); }); diff --git a/src/modules/geography/core/application/ports/direction-encoder.port.ts b/src/modules/geography/core/application/ports/direction-encoder.port.ts index 737456a..316de85 100644 --- a/src/modules/geography/core/application/ports/direction-encoder.port.ts +++ b/src/modules/geography/core/application/ports/direction-encoder.port.ts @@ -1,6 +1,6 @@ -import { Coordinates } from '../../domain/route.types'; +import { Point } from '../../domain/route.types'; export interface DirectionEncoderPort { - encode(coordinates: Coordinates[]): string; - decode(direction: string): Coordinates[]; + encode(coordinates: Point[]): string; + decode(direction: string): Point[]; } diff --git a/src/modules/geography/core/application/ports/georouter.port.ts b/src/modules/geography/core/application/ports/georouter.port.ts index 1990e99..8936ff0 100644 --- a/src/modules/geography/core/application/ports/georouter.port.ts +++ b/src/modules/geography/core/application/ports/georouter.port.ts @@ -1,6 +1,6 @@ -import { Path, Route } from '../../domain/route.types'; +import { Route, Waypoint } from '../../domain/route.types'; import { GeorouterSettings } from '../types/georouter-settings.type'; export interface GeorouterPort { - routes(paths: Path[], settings: GeorouterSettings): Promise; + route(waypoints: Waypoint[], settings: GeorouterSettings): Promise; } diff --git a/src/modules/geography/core/application/queries/get-route/get-route.query-handler.ts b/src/modules/geography/core/application/queries/get-route/get-route.query-handler.ts index 1a255aa..77d88ad 100644 --- a/src/modules/geography/core/application/queries/get-route/get-route.query-handler.ts +++ b/src/modules/geography/core/application/queries/get-route/get-route.query-handler.ts @@ -11,7 +11,6 @@ export class GetRouteQueryHandler implements IQueryHandler { execute = async (query: GetRouteQuery): Promise => await RouteEntity.create({ - roles: query.roles, waypoints: query.waypoints, georouter: this.georouter, georouterSettings: query.georouterSettings, diff --git a/src/modules/geography/core/application/queries/get-route/get-route.query.ts b/src/modules/geography/core/application/queries/get-route/get-route.query.ts index eef3ed1..0c936d9 100644 --- a/src/modules/geography/core/application/queries/get-route/get-route.query.ts +++ b/src/modules/geography/core/application/queries/get-route/get-route.query.ts @@ -1,19 +1,13 @@ import { QueryBase } from '@mobicoop/ddd-library'; -import { Role, Waypoint } from '@modules/geography/core/domain/route.types'; +import { Waypoint } from '@modules/geography/core/domain/route.types'; import { GeorouterSettings } from '../../types/georouter-settings.type'; export class GetRouteQuery extends QueryBase { - readonly roles: Role[]; readonly waypoints: Waypoint[]; readonly georouterSettings: GeorouterSettings; - constructor( - roles: Role[], - waypoints: Waypoint[], - georouterSettings: GeorouterSettings, - ) { + constructor(waypoints: Waypoint[], georouterSettings: GeorouterSettings) { super(); - this.roles = roles; this.waypoints = waypoints; this.georouterSettings = georouterSettings; } diff --git a/src/modules/geography/core/domain/route.entity.ts b/src/modules/geography/core/domain/route.entity.ts index 9bbee40..373d3aa 100644 --- a/src/modules/geography/core/domain/route.entity.ts +++ b/src/modules/geography/core/domain/route.entity.ts @@ -1,13 +1,5 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; -import { - CreateRouteProps, - Path, - Role, - RouteProps, - PathType, - Route, -} from './route.types'; -import { WaypointProps } from './value-objects/waypoint.value-object'; +import { CreateRouteProps, RouteProps, Route } from './route.types'; import { v4 } from 'uuid'; import { RouteNotFoundException } from './route.errors'; @@ -15,43 +7,18 @@ export class RouteEntity extends AggregateRoot { protected readonly _id: AggregateID; static create = async (create: CreateRouteProps): Promise => { - const routes: Route[] = await create.georouter.routes( - this.getPaths(create.roles, create.waypoints), + const route: Route = await create.georouter.route( + create.waypoints, create.georouterSettings, ); - if (!routes || routes.length == 0) throw new RouteNotFoundException(); - let baseRoute: Route; - let driverRoute: Route | undefined; - let passengerRoute: Route | undefined; - if (routes.some((route: Route) => route.type == PathType.GENERIC)) { - driverRoute = passengerRoute = routes.find( - (route: Route) => route.type == PathType.GENERIC, - ); - } else { - driverRoute = routes.some((route: Route) => route.type == PathType.DRIVER) - ? routes.find((route: Route) => route.type == PathType.DRIVER) - : undefined; - passengerRoute = routes.some( - (route: Route) => route.type == PathType.PASSENGER, - ) - ? routes.find((route: Route) => route.type == PathType.PASSENGER) - : undefined; - } - if (driverRoute) { - baseRoute = driverRoute; - } else { - baseRoute = passengerRoute as Route; - } + if (!route) throw new RouteNotFoundException(); const routeProps: RouteProps = { - driverDistance: driverRoute?.distance, - driverDuration: driverRoute?.duration, - passengerDistance: passengerRoute?.distance, - passengerDuration: passengerRoute?.duration, - fwdAzimuth: baseRoute.fwdAzimuth, - backAzimuth: baseRoute.backAzimuth, - distanceAzimuth: baseRoute.distanceAzimuth, - waypoints: create.waypoints, - points: baseRoute.points, + distance: route.distance, + duration: route.duration, + fwdAzimuth: route.fwdAzimuth, + backAzimuth: route.backAzimuth, + distanceAzimuth: route.distanceAzimuth, + points: route.points, }; return new RouteEntity({ id: v4(), @@ -63,46 +30,46 @@ export class RouteEntity extends AggregateRoot { // entity business rules validation to protect it's invariant before saving entity to a database } - private static getPaths = ( - roles: Role[], - waypoints: WaypointProps[], - ): Path[] => { - const paths: Path[] = []; - if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { - if (waypoints.length == 2) { - // 2 points => same route for driver and passenger - paths.push(this.createGenericPath(waypoints)); - } else { - paths.push( - this.createDriverPath(waypoints), - this.createPassengerPath(waypoints), - ); - } - } else if (roles.includes(Role.DRIVER)) { - paths.push(this.createDriverPath(waypoints)); - } else if (roles.includes(Role.PASSENGER)) { - paths.push(this.createPassengerPath(waypoints)); - } - return paths; - }; + // private static getPaths = ( + // roles: Role[], + // waypoints: WaypointProps[], + // ): Path[] => { + // const paths: Path[] = []; + // if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { + // if (waypoints.length == 2) { + // // 2 points => same route for driver and passenger + // paths.push(this.createGenericPath(waypoints)); + // } else { + // paths.push( + // this.createDriverPath(waypoints), + // this.createPassengerPath(waypoints), + // ); + // } + // } else if (roles.includes(Role.DRIVER)) { + // paths.push(this.createDriverPath(waypoints)); + // } else if (roles.includes(Role.PASSENGER)) { + // paths.push(this.createPassengerPath(waypoints)); + // } + // return paths; + // }; - private static createGenericPath = (waypoints: WaypointProps[]): Path => - this.createPath(waypoints, PathType.GENERIC); + // private static createGenericPath = (waypoints: WaypointProps[]): Path => + // this.createPath(waypoints, PathType.GENERIC); - private static createDriverPath = (waypoints: WaypointProps[]): Path => - this.createPath(waypoints, PathType.DRIVER); + // private static createDriverPath = (waypoints: WaypointProps[]): Path => + // this.createPath(waypoints, PathType.DRIVER); - private static createPassengerPath = (waypoints: WaypointProps[]): Path => - this.createPath( - [waypoints[0], waypoints[waypoints.length - 1]], - PathType.PASSENGER, - ); + // private static createPassengerPath = (waypoints: WaypointProps[]): Path => + // this.createPath( + // [waypoints[0], waypoints[waypoints.length - 1]], + // PathType.PASSENGER, + // ); - private static createPath = ( - points: WaypointProps[], - type: PathType, - ): Path => ({ - type, - points, - }); + // private static createPath = ( + // points: WaypointProps[], + // type: PathType, + // ): Path => ({ + // type, + // points, + // }); } diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index 748c759..9340718 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -1,67 +1,49 @@ import { GeorouterPort } from '../application/ports/georouter.port'; import { GeorouterSettings } from '../application/types/georouter-settings.type'; -import { CoordinatesProps } from './value-objects/coordinates.value-object'; -import { SpacetimePointProps } from './value-objects/spacetime-point.value-object'; +import { PointProps } from './value-objects/point.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object'; // All properties that a Route has export interface RouteProps { - driverDistance?: number; - driverDuration?: number; - passengerDistance?: number; - passengerDuration?: number; + distance: number; + duration: number; fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - waypoints: WaypointProps[]; - points: SpacetimePointProps[] | CoordinatesProps[]; + points: PointProps[]; } // Properties that are needed for a Route creation export interface CreateRouteProps { - roles: Role[]; waypoints: WaypointProps[]; georouter: GeorouterPort; georouterSettings: GeorouterSettings; } export type Route = { - type: PathType; distance: number; duration: number; fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - points: Coordinates[]; - spacetimeWaypoints: SpacetimePoint[]; + points: Point[]; + steps: Step[]; }; -export type Path = { - type: PathType; - points: Coordinates[]; -}; - -export type Coordinates = { +export type Point = { lon: number; lat: number; }; -export type Waypoint = Coordinates & { +export type Waypoint = Point & { position: number; }; -export type SpacetimePoint = Coordinates & { +export type Spacetime = { duration: number; distance?: number; }; -export enum Role { - DRIVER = 'DRIVER', - PASSENGER = 'PASSENGER', -} +export type Step = Point & Spacetime; -export enum PathType { - GENERIC = 'generic', - DRIVER = 'driver', - PASSENGER = 'passenger', -} +export type Waystep = Waypoint & Spacetime; diff --git a/src/modules/geography/core/domain/value-objects/coordinates.value-object.ts b/src/modules/geography/core/domain/value-objects/point.value-object.ts similarity index 79% rename from src/modules/geography/core/domain/value-objects/coordinates.value-object.ts rename to src/modules/geography/core/domain/value-objects/point.value-object.ts index 9bd81e7..2047ead 100644 --- a/src/modules/geography/core/domain/value-objects/coordinates.value-object.ts +++ b/src/modules/geography/core/domain/value-objects/point.value-object.ts @@ -8,12 +8,12 @@ import { * other Value Objects inside if needed. * */ -export interface CoordinatesProps { +export interface PointProps { lon: number; lat: number; } -export class Coordinates extends ValueObject { +export class Point extends ValueObject { get lon(): number { return this.props.lon; } @@ -22,7 +22,7 @@ export class Coordinates extends ValueObject { return this.props.lat; } - protected validate(props: CoordinatesProps): void { + protected validate(props: PointProps): void { if (props.lon > 180 || props.lon < -180) throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); if (props.lat > 90 || props.lat < -90) diff --git a/src/modules/geography/core/domain/value-objects/spacetime-point.value-object.ts b/src/modules/geography/core/domain/value-objects/step.value-object.ts similarity index 86% rename from src/modules/geography/core/domain/value-objects/spacetime-point.value-object.ts rename to src/modules/geography/core/domain/value-objects/step.value-object.ts index c7bfbce..a5c180f 100644 --- a/src/modules/geography/core/domain/value-objects/spacetime-point.value-object.ts +++ b/src/modules/geography/core/domain/value-objects/step.value-object.ts @@ -9,14 +9,14 @@ import { * other Value Objects inside if needed. * */ -export interface SpacetimePointProps { +export interface StepProps { lon: number; lat: number; duration: number; distance: number; } -export class SpacetimePoint extends ValueObject { +export class Step extends ValueObject { get lon(): number { return this.props.lon; } @@ -33,7 +33,7 @@ export class SpacetimePoint extends ValueObject { return this.props.distance; } - protected validate(props: SpacetimePointProps): void { + protected validate(props: StepProps): void { if (props.duration < 0) throw new ArgumentInvalidException( 'duration must be greater than or equal to 0', diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts index 37278d6..adfc60a 100644 --- a/src/modules/geography/infrastructure/graphhopper-georouter.ts +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -2,12 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { GeorouterPort } from '../core/application/ports/georouter.port'; import { GeorouterSettings } from '../core/application/types/georouter-settings.type'; -import { - Path, - PathType, - Route, - SpacetimePoint, -} from '../core/domain/route.types'; +import { Route, Step, Waypoint } from '../core/domain/route.types'; import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; import { GEODESIC, PARAMS_PROVIDER } from '../geography.di-tokens'; import { catchError, lastValueFrom, map } from 'rxjs'; @@ -35,13 +30,13 @@ export class GraphhopperGeorouter implements GeorouterPort { ].join(''); } - routes = async ( - paths: Path[], + route = async ( + waypoints: Waypoint[], settings: GeorouterSettings, - ): Promise => { + ): Promise => { this._setDefaultUrlArgs(); this._setSettings(settings); - return this._getRoutes(paths); + return this._getRoute(waypoints); }; private _setDefaultUrlArgs = (): void => { @@ -62,46 +57,39 @@ export class GraphhopperGeorouter implements GeorouterPort { } }; - private _getRoutes = async (paths: Path[]): Promise => { - const routes = Promise.all( - paths.map(async (path) => { - const url: string = [ - this.getUrl(), - '&point=', - path.points - .map((point) => [point.lat, point.lon].join('%2C')) - .join('&point='), - ].join(''); - return await lastValueFrom( - this.httpService.get(url).pipe( - map((response) => { - if (response.data) return this.createRoute(response, path.type); - throw new Error(); - }), - catchError((error: AxiosError) => { - if (error.code == AxiosError.ERR_BAD_REQUEST) { - throw new RouteNotFoundException( - error, - 'No route found for given coordinates', - ); - } - throw new GeorouterUnavailableException(error); - }), - ), - ); - }), + private _getRoute = async (waypoints: Waypoint[]): Promise => { + const url: string = [ + this.getUrl(), + '&point=', + waypoints + .map((waypoint: Waypoint) => [waypoint.lat, waypoint.lon].join('%2C')) + .join('&point='), + ].join(''); + return await lastValueFrom( + this.httpService.get(url).pipe( + map((response) => { + if (response.data) return this.createRoute(response); + throw new Error(); + }), + catchError((error: AxiosError) => { + if (error.code == AxiosError.ERR_BAD_REQUEST) { + throw new RouteNotFoundException( + error, + 'No route found for given coordinates', + ); + } + throw new GeorouterUnavailableException(error); + }), + ), ); - return routes; }; private getUrl = (): string => [this.url, this.urlArgs.join('&')].join(''); private createRoute = ( response: AxiosResponse, - type: PathType, ): Route => { const route = {} as Route; - route.type = type; if (response.data.paths && response.data.paths[0]) { const shortestPath = response.data.paths[0]; route.distance = shortestPath.distance ?? 0; @@ -135,7 +123,7 @@ export class GraphhopperGeorouter implements GeorouterPort { let instructions: GraphhopperInstruction[] = []; if (shortestPath.instructions) instructions = shortestPath.instructions; - route.spacetimeWaypoints = this.generateSpacetimePoints( + route.steps = this.generateSteps( shortestPath.points.coordinates, shortestPath.snapped_waypoints.coordinates, shortestPath.details.time, @@ -147,12 +135,12 @@ export class GraphhopperGeorouter implements GeorouterPort { return route; }; - private generateSpacetimePoints = ( + private generateSteps = ( points: [[number, number]], snappedWaypoints: [[number, number]], durations: [[number, number, number]], instructions: GraphhopperInstruction[], - ): SpacetimePoint[] => { + ): Step[] => { const indices = this.getIndices(points, snappedWaypoints); const times = this.getTimes(durations, indices); const distances = this.getDistances(instructions, indices); diff --git a/src/modules/geography/infrastructure/postgres-direction-encoder.ts b/src/modules/geography/infrastructure/postgres-direction-encoder.ts index d6cb0b6..05cae0b 100644 --- a/src/modules/geography/infrastructure/postgres-direction-encoder.ts +++ b/src/modules/geography/infrastructure/postgres-direction-encoder.ts @@ -1,16 +1,16 @@ import { DirectionEncoderPort } from '../core/application/ports/direction-encoder.port'; import { Injectable } from '@nestjs/common'; -import { Coordinates } from '../core/domain/route.types'; +import { Point } from '../core/domain/route.types'; @Injectable() export class PostgresDirectionEncoder implements DirectionEncoderPort { - encode = (coordinates: Coordinates[]): string => + encode = (coordinates: Point[]): string => [ "'LINESTRING(", coordinates.map((point) => [point.lon, point.lat].join(' ')).join(), ")'", ].join(''); - decode = (direction: string): Coordinates[] => + decode = (direction: string): Point[] => direction .split('(')[1] .split(')')[0] diff --git a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts index 7790bba..1b01076 100644 --- a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts +++ b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts @@ -1,6 +1,5 @@ -import { Role, Waypoint } from '@modules/geography/core/domain/route.types'; +import { Waypoint } from '@modules/geography/core/domain/route.types'; export type GetRouteRequestDto = { - roles: Role[]; waypoints: Waypoint[]; }; diff --git a/src/modules/geography/interface/controllers/get-basic-route.controller.ts b/src/modules/geography/interface/controllers/get-basic-route.controller.ts index 3bdc42e..3c14b10 100644 --- a/src/modules/geography/interface/controllers/get-basic-route.controller.ts +++ b/src/modules/geography/interface/controllers/get-basic-route.controller.ts @@ -16,7 +16,7 @@ export class GetBasicRouteController implements GetBasicRouteControllerPort { async get(data: GetRouteRequestDto): Promise { const route: RouteEntity = await this.queryBus.execute( - new GetRouteQuery(data.roles, data.waypoints, { + new GetRouteQuery(data.waypoints, { detailedDistance: false, detailedDuration: false, points: true, diff --git a/src/modules/geography/interface/dtos/route.response.dto.ts b/src/modules/geography/interface/dtos/route.response.dto.ts index 714fb68..c157585 100644 --- a/src/modules/geography/interface/dtos/route.response.dto.ts +++ b/src/modules/geography/interface/dtos/route.response.dto.ts @@ -1,15 +1,10 @@ -import { - Coordinates, - SpacetimePoint, -} from '@modules/geography/core/domain/route.types'; +import { Point, Step } from '@modules/geography/core/domain/route.types'; export class RouteResponseDto { - driverDistance?: number; - driverDuration?: number; - passengerDistance?: number; - passengerDuration?: number; + distance?: number; + duration?: number; fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - points: SpacetimePoint[] | Coordinates[]; + points: Step[] | Point[]; } diff --git a/src/modules/geography/route.mapper.ts b/src/modules/geography/route.mapper.ts index a714ff5..c7d6eef 100644 --- a/src/modules/geography/route.mapper.ts +++ b/src/modules/geography/route.mapper.ts @@ -16,17 +16,11 @@ export class RouteMapper { toResponse = (entity: RouteEntity): RouteResponseDto => { const response = new RouteResponseDto(); - response.driverDistance = entity.getProps().driverDistance - ? Math.round(entity.getProps().driverDistance as number) + response.distance = entity.getProps().distance + ? Math.round(entity.getProps().distance as number) : undefined; - response.driverDuration = entity.getProps().driverDuration - ? Math.round(entity.getProps().driverDuration as number) - : undefined; - response.passengerDistance = entity.getProps().passengerDistance - ? Math.round(entity.getProps().passengerDistance as number) - : undefined; - response.passengerDuration = entity.getProps().passengerDuration - ? Math.round(entity.getProps().passengerDuration as number) + response.duration = entity.getProps().duration + ? Math.round(entity.getProps().duration as number) : undefined; response.fwdAzimuth = Math.round(entity.getProps().fwdAzimuth); response.backAzimuth = Math.round(entity.getProps().backAzimuth); diff --git a/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts b/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts index 948ed0e..8008540 100644 --- a/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts +++ b/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts @@ -2,7 +2,7 @@ import { GeorouterPort } from '@modules/geography/core/application/ports/georout import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query'; import { GetRouteQueryHandler } from '@modules/geography/core/application/queries/get-route/get-route.query-handler'; import { RouteEntity } from '@modules/geography/core/domain/route.entity'; -import { Role, Waypoint } from '@modules/geography/core/domain/route.types'; +import { Waypoint } from '@modules/geography/core/domain/route.types'; import { GEOROUTER } from '@modules/geography/geography.di-tokens'; import { Test, TestingModule } from '@nestjs/testing'; @@ -18,7 +18,7 @@ const destinationWaypoint: Waypoint = { }; const mockGeorouter: GeorouterPort = { - routes: jest.fn(), + route: jest.fn(), }; describe('Get route query handler', () => { @@ -44,9 +44,8 @@ describe('Get route query handler', () => { }); describe('execution', () => { - it('should get a route for a driver only', async () => { + it('should get a route', async () => { const getRoutequery = new GetRouteQuery( - [Role.DRIVER], [originWaypoint, destinationWaypoint], { detailedDistance: false, diff --git a/src/modules/geography/tests/unit/core/coordinates.value-object.spec.ts b/src/modules/geography/tests/unit/core/point.value-object.spec.ts similarity index 67% rename from src/modules/geography/tests/unit/core/coordinates.value-object.spec.ts rename to src/modules/geography/tests/unit/core/point.value-object.spec.ts index 4c7b1f7..8fc5573 100644 --- a/src/modules/geography/tests/unit/core/coordinates.value-object.spec.ts +++ b/src/modules/geography/tests/unit/core/point.value-object.spec.ts @@ -1,18 +1,18 @@ import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; -import { Coordinates } from '@modules/geography/core/domain/value-objects/coordinates.value-object'; +import { Point } from '@modules/geography/core/domain/value-objects/point.value-object'; -describe('Waypoint value object', () => { - it('should create a waypoint value object', () => { - const coordinatesVO = new Coordinates({ +describe('Point value object', () => { + it('should create a point value object', () => { + const pointVO = new Point({ lat: 48.689445, lon: 6.17651, }); - expect(coordinatesVO.lat).toBe(48.689445); - expect(coordinatesVO.lon).toBe(6.17651); + expect(pointVO.lat).toBe(48.689445); + expect(pointVO.lon).toBe(6.17651); }); it('should throw an exception if longitude is invalid', () => { try { - new Coordinates({ + new Point({ lat: 48.689445, lon: 186.17651, }); @@ -20,7 +20,7 @@ describe('Waypoint value object', () => { expect(e).toBeInstanceOf(ArgumentOutOfRangeException); } try { - new Coordinates({ + new Point({ lat: 48.689445, lon: -186.17651, }); @@ -30,7 +30,7 @@ describe('Waypoint value object', () => { }); it('should throw an exception if latitude is invalid', () => { try { - new Coordinates({ + new Point({ lat: 148.689445, lon: 6.17651, }); @@ -38,7 +38,7 @@ describe('Waypoint value object', () => { expect(e).toBeInstanceOf(ArgumentOutOfRangeException); } try { - new Coordinates({ + new Point({ lat: -148.689445, lon: 6.17651, }); diff --git a/src/modules/geography/tests/unit/core/route.entity.spec.ts b/src/modules/geography/tests/unit/core/route.entity.spec.ts index 7cf810a..127cf36 100644 --- a/src/modules/geography/tests/unit/core/route.entity.spec.ts +++ b/src/modules/geography/tests/unit/core/route.entity.spec.ts @@ -2,205 +2,56 @@ import { GeorouterPort } from '@modules/geography/core/application/ports/georout import { RouteEntity } from '@modules/geography/core/domain/route.entity'; import { RouteNotFoundException } from '@modules/geography/core/domain/route.errors'; import { - Coordinates, + Point, CreateRouteProps, - PathType, - Role, } from '@modules/geography/core/domain/route.types'; -const originCoordinates: Coordinates = { +const originPoint: Point = { lat: 48.689445, lon: 6.17651, }; -const destinationCoordinates: Coordinates = { +const destinationPoint: Point = { lat: 48.8566, lon: 2.3522, }; -const additionalCoordinates: Coordinates = { - lon: 48.7566, - lat: 4.4498, -}; const mockGeorouter: GeorouterPort = { - routes: jest + route: jest .fn() - .mockImplementationOnce(() => [ - { - type: PathType.DRIVER, - distance: 350101, - duration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336544, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - spacetimePoints: [], - }, - ]) - .mockImplementationOnce(() => [ - { - type: PathType.PASSENGER, - distance: 350102, - duration: 14423, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336545, - points: [ - { - lon: 6.1765103, - lat: 48.689446, - }, - { - lon: 4.984579, - lat: 48.725688, - }, - { - lon: 2.3523, - lat: 48.8567, - }, - ], - spacetimePoints: [], - }, - ]) - .mockImplementationOnce(() => [ - { - type: PathType.GENERIC, - distance: 350100, - duration: 14421, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336543, - points: [ - { - lon: 6.1765101, - lat: 48.689444, - }, - { - lon: 4.984577, - lat: 48.725686, - }, - { - lon: 2.3521, - lat: 48.8565, - }, - ], - spacetimePoints: [], - }, - ]) - .mockImplementationOnce(() => [ - { - type: PathType.GENERIC, - distance: 350108, - duration: 14428, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336548, - points: [ - { - lon: 6.1765101, - lat: 48.689444, - }, - { - lon: 4.984577, - lat: 48.725686, - }, - { - lon: 2.3521, - lat: 48.8565, - }, - ], - spacetimePoints: [], - }, - ]) + .mockImplementationOnce(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + steps: [], + })) .mockImplementationOnce(() => []), }; -const createDriverRouteProps: CreateRouteProps = { - roles: [Role.DRIVER], +const createRouteProps: CreateRouteProps = { waypoints: [ { position: 0, - ...originCoordinates, + ...originPoint, }, { position: 1, - ...destinationCoordinates, - }, - ], - georouter: mockGeorouter, - georouterSettings: { - points: true, - detailedDistance: false, - detailedDuration: false, - }, -}; - -const createPassengerRouteProps: CreateRouteProps = { - roles: [Role.PASSENGER], - waypoints: [ - { - position: 0, - ...originCoordinates, - }, - { - position: 1, - ...destinationCoordinates, - }, - ], - georouter: mockGeorouter, - georouterSettings: { - points: true, - detailedDistance: false, - detailedDuration: false, - }, -}; - -const createSimpleDriverAndPassengerRouteProps: CreateRouteProps = { - roles: [Role.DRIVER, Role.PASSENGER], - waypoints: [ - { - position: 0, - ...originCoordinates, - }, - { - position: 1, - ...destinationCoordinates, - }, - ], - georouter: mockGeorouter, - georouterSettings: { - points: true, - detailedDistance: false, - detailedDuration: false, - }, -}; - -const createComplexDriverAndPassengerRouteProps: CreateRouteProps = { - roles: [Role.DRIVER, Role.PASSENGER], - waypoints: [ - { - position: 0, - ...originCoordinates, - }, - { - position: 1, - ...additionalCoordinates, - }, - { - position: 2, - ...destinationCoordinates, + ...destinationPoint, }, ], georouter: mockGeorouter, @@ -212,43 +63,15 @@ const createComplexDriverAndPassengerRouteProps: CreateRouteProps = { }; describe('Route entity create', () => { - it('should create a new entity for a driver only', async () => { - const route: RouteEntity = await RouteEntity.create(createDriverRouteProps); + it('should create a new entity', async () => { + const route: RouteEntity = await RouteEntity.create(createRouteProps); expect(route.id.length).toBe(36); - expect(route.getProps().driverDuration).toBe(14422); - expect(route.getProps().passengerDistance).toBeUndefined(); - }); - it('should create a new entity for a passenger only', async () => { - const route: RouteEntity = await RouteEntity.create( - createPassengerRouteProps, - ); - expect(route.id.length).toBe(36); - expect(route.getProps().passengerDuration).toBe(14423); - expect(route.getProps().driverDistance).toBeUndefined(); - }); - it('should create a new entity for a simple driver and passenger route', async () => { - const route: RouteEntity = await RouteEntity.create( - createSimpleDriverAndPassengerRouteProps, - ); - expect(route.id.length).toBe(36); - expect(route.getProps().driverDuration).toBe(14421); - expect(route.getProps().driverDistance).toBe(350100); - expect(route.getProps().passengerDuration).toBe(14421); - expect(route.getProps().passengerDistance).toBe(350100); - }); - it('should create a new entity for a complex driver and passenger route', async () => { - const route: RouteEntity = await RouteEntity.create( - createComplexDriverAndPassengerRouteProps, - ); - expect(route.id.length).toBe(36); - expect(route.getProps().driverDuration).toBe(14428); - expect(route.getProps().driverDistance).toBe(350108); - expect(route.getProps().passengerDuration).toBe(14428); - expect(route.getProps().passengerDistance).toBe(350108); + expect(route.getProps().duration).toBe(14422); }); + it('should throw an exception if route is not found', async () => { try { - await RouteEntity.create(createDriverRouteProps); + await RouteEntity.create(createRouteProps); } catch (e: any) { expect(e).toBeInstanceOf(RouteNotFoundException); } diff --git a/src/modules/geography/tests/unit/core/spacetime-point.value-object.spec.ts b/src/modules/geography/tests/unit/core/step.value-object.spec.ts similarity index 74% rename from src/modules/geography/tests/unit/core/spacetime-point.value-object.spec.ts rename to src/modules/geography/tests/unit/core/step.value-object.spec.ts index 405190c..6811c97 100644 --- a/src/modules/geography/tests/unit/core/spacetime-point.value-object.spec.ts +++ b/src/modules/geography/tests/unit/core/step.value-object.spec.ts @@ -2,24 +2,24 @@ import { ArgumentInvalidException, ArgumentOutOfRangeException, } from '@mobicoop/ddd-library'; -import { SpacetimePoint } from '@modules/geography/core/domain/value-objects/spacetime-point.value-object'; +import { Step } from '@modules/geography/core/domain/value-objects/step.value-object'; -describe('Timepoint value object', () => { - it('should create a timepoint value object', () => { - const timepointVO = new SpacetimePoint({ +describe('Step value object', () => { + it('should create a step value object', () => { + const stepVO = new Step({ lat: 48.689445, lon: 6.17651, duration: 150, distance: 12000, }); - expect(timepointVO.duration).toBe(150); - expect(timepointVO.distance).toBe(12000); - expect(timepointVO.lat).toBe(48.689445); - expect(timepointVO.lon).toBe(6.17651); + expect(stepVO.duration).toBe(150); + expect(stepVO.distance).toBe(12000); + expect(stepVO.lat).toBe(48.689445); + expect(stepVO.lon).toBe(6.17651); }); it('should throw an exception if longitude is invalid', () => { try { - new SpacetimePoint({ + new Step({ lat: 48.689445, lon: 186.17651, duration: 150, @@ -29,7 +29,7 @@ describe('Timepoint value object', () => { expect(e).toBeInstanceOf(ArgumentOutOfRangeException); } try { - new SpacetimePoint({ + new Step({ lon: 48.689445, lat: -186.17651, duration: 150, @@ -41,7 +41,7 @@ describe('Timepoint value object', () => { }); it('should throw an exception if latitude is invalid', () => { try { - new SpacetimePoint({ + new Step({ lat: 248.689445, lon: 6.17651, duration: 150, @@ -51,7 +51,7 @@ describe('Timepoint value object', () => { expect(e).toBeInstanceOf(ArgumentOutOfRangeException); } try { - new SpacetimePoint({ + new Step({ lon: -148.689445, lat: 6.17651, duration: 150, @@ -63,7 +63,7 @@ describe('Timepoint value object', () => { }); it('should throw an exception if distance is invalid', () => { try { - new SpacetimePoint({ + new Step({ lat: 48.689445, lon: 6.17651, duration: 150, @@ -75,7 +75,7 @@ describe('Timepoint value object', () => { }); it('should throw an exception if duration is invalid', () => { try { - new SpacetimePoint({ + new Step({ lat: 48.689445, lon: 6.17651, duration: -150, diff --git a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts index c167240..1a40b5d 100644 --- a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts @@ -4,7 +4,7 @@ import { GeorouterUnavailableException, RouteNotFoundException, } from '@modules/geography/core/domain/route.errors'; -import { PathType, Route } from '@modules/geography/core/domain/route.types'; +import { Route } from '@modules/geography/core/domain/route.types'; import { GEODESIC, PARAMS_PROVIDER, @@ -294,20 +294,17 @@ describe('Graphhopper Georouter', () => { it('should fail if route is not found', async () => { await expect( - graphhopperGeorouter.routes( + graphhopperGeorouter.route( [ { - type: PathType.DRIVER, - points: [ - { - lon: 0, - lat: 0, - }, - { - lon: 1, - lat: 1, - }, - ], + position: 0, + lon: 0, + lat: 0, + }, + { + position: 1, + lon: 1, + lat: 1, }, ], { @@ -321,20 +318,17 @@ describe('Graphhopper Georouter', () => { it('should fail if georouter is unavailable', async () => { await expect( - graphhopperGeorouter.routes( + graphhopperGeorouter.route( [ { - type: PathType.DRIVER, - points: [ - { - lon: 0, - lat: 0, - }, - { - lon: 1, - lat: 1, - }, - ], + position: 0, + lon: 0, + lat: 0, + }, + { + position: 1, + lon: 1, + lat: 1, }, ], { @@ -347,20 +341,17 @@ describe('Graphhopper Georouter', () => { }); it('should create a basic route', async () => { - const routes: Route[] = await graphhopperGeorouter.routes( + const route: Route = await graphhopperGeorouter.route( [ { - type: PathType.DRIVER, - points: [ - { - lon: 0, - lat: 0, - }, - { - lon: 10, - lat: 10, - }, - ], + position: 0, + lon: 0, + lat: 0, + }, + { + position: 1, + lon: 10, + lat: 10, }, ], { @@ -369,25 +360,21 @@ describe('Graphhopper Georouter', () => { points: false, }, ); - expect(routes).toHaveLength(1); - expect(routes[0].distance).toBe(50000); + expect(route.distance).toBe(50000); }); - it('should create one route with points', async () => { - const routes = await graphhopperGeorouter.routes( + it('should create a route with points', async () => { + const route: Route = await graphhopperGeorouter.route( [ { - type: PathType.DRIVER, - points: [ - { - lat: 0, - lon: 0, - }, - { - lat: 10, - lon: 10, - }, - ], + position: 0, + lon: 0, + lat: 0, + }, + { + position: 1, + lon: 10, + lat: 10, }, ], { @@ -396,29 +383,25 @@ describe('Graphhopper Georouter', () => { points: true, }, ); - expect(routes).toHaveLength(1); - expect(routes[0].distance).toBe(50000); - expect(routes[0].duration).toBe(1800); - expect(routes[0].fwdAzimuth).toBe(45); - expect(routes[0].backAzimuth).toBe(225); - expect(routes[0].points).toHaveLength(11); + expect(route.distance).toBe(50000); + expect(route.duration).toBe(1800); + expect(route.fwdAzimuth).toBe(45); + expect(route.backAzimuth).toBe(225); + expect(route.points).toHaveLength(11); }); - it('should create one route with points and time', async () => { - const routes = await graphhopperGeorouter.routes( + it('should create a route with points and time', async () => { + const route: Route = await graphhopperGeorouter.route( [ { - type: PathType.DRIVER, - points: [ - { - lat: 0, - lon: 0, - }, - { - lat: 10, - lon: 10, - }, - ], + position: 0, + lon: 0, + lat: 0, + }, + { + position: 1, + lon: 10, + lat: 10, }, ], { @@ -427,31 +410,28 @@ describe('Graphhopper Georouter', () => { points: true, }, ); - expect(routes).toHaveLength(1); - expect(routes[0].spacetimeWaypoints).toHaveLength(2); - expect(routes[0].spacetimeWaypoints[1].duration).toBe(1800); - expect(routes[0].spacetimeWaypoints[1].distance).toBeUndefined(); + expect(route.steps).toHaveLength(2); + expect(route.steps[1].duration).toBe(1800); + expect(route.steps[1].distance).toBeUndefined(); }); it('should create one route with points and missed waypoints extrapolations', async () => { - const routes = await graphhopperGeorouter.routes( + const route: Route = await graphhopperGeorouter.route( [ { - type: PathType.DRIVER, - points: [ - { - lat: 0, - lon: 0, - }, - { - lat: 5, - lon: 5, - }, - { - lat: 10, - lon: 10, - }, - ], + position: 0, + lon: 0, + lat: 0, + }, + { + position: 1, + lon: 5, + lat: 5, + }, + { + position: 2, + lon: 10, + lat: 10, }, ], { @@ -460,30 +440,26 @@ describe('Graphhopper Georouter', () => { points: true, }, ); - expect(routes).toHaveLength(1); - expect(routes[0].spacetimeWaypoints).toHaveLength(3); - expect(routes[0].distance).toBe(50000); - expect(routes[0].duration).toBe(1800); - expect(routes[0].fwdAzimuth).toBe(45); - expect(routes[0].backAzimuth).toBe(225); - expect(routes[0].points.length).toBe(9); + expect(route.steps).toHaveLength(3); + expect(route.distance).toBe(50000); + expect(route.duration).toBe(1800); + expect(route.fwdAzimuth).toBe(45); + expect(route.backAzimuth).toBe(225); + expect(route.points.length).toBe(9); }); - it('should create one route with points, time and distance', async () => { - const routes = await graphhopperGeorouter.routes( + it('should create a route with points, time and distance', async () => { + const route: Route = await graphhopperGeorouter.route( [ { - type: PathType.DRIVER, - points: [ - { - lat: 0, - lon: 0, - }, - { - lat: 10, - lon: 10, - }, - ], + position: 0, + lon: 0, + lat: 0, + }, + { + position: 1, + lon: 10, + lat: 10, }, ], { @@ -492,9 +468,8 @@ describe('Graphhopper Georouter', () => { points: true, }, ); - expect(routes).toHaveLength(1); - expect(routes[0].spacetimeWaypoints.length).toBe(3); - expect(routes[0].spacetimeWaypoints[1].duration).toBe(990); - expect(routes[0].spacetimeWaypoints[1].distance).toBe(25000); + expect(route.steps.length).toBe(3); + expect(route.steps[1].duration).toBe(990); + expect(route.steps[1].distance).toBe(25000); }); }); diff --git a/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts b/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts index fd4cbab..8a2527c 100644 --- a/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/postgres-direction-encoder.spec.ts @@ -1,4 +1,4 @@ -import { Coordinates } from '@modules/geography/core/domain/route.types'; +import { Point } from '@modules/geography/core/domain/route.types'; import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; describe('Postgres direction encoder', () => { @@ -7,10 +7,10 @@ describe('Postgres direction encoder', () => { new PostgresDirectionEncoder(); expect(postgresDirectionEncoder).toBeDefined(); }); - it('should encode coordinates to a postgres direction', () => { + it('should encode points to a postgres direction', () => { const postgresDirectionEncoder: PostgresDirectionEncoder = new PostgresDirectionEncoder(); - const coordinates: Coordinates[] = [ + const points: Point[] = [ { lon: 6, lat: 47, @@ -24,18 +24,17 @@ describe('Postgres direction encoder', () => { lat: 47.2, }, ]; - const direction = postgresDirectionEncoder.encode(coordinates); + const direction = postgresDirectionEncoder.encode(points); expect(direction).toBe("'LINESTRING(6 47,6.1 47.1,6.2 47.2)'"); }); it('should decode a postgres direction to coordinates', () => { const postgresDirectionEncoder: PostgresDirectionEncoder = new PostgresDirectionEncoder(); const direction = "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'"; - const coordinates: Coordinates[] = - postgresDirectionEncoder.decode(direction); - expect(coordinates.length).toBe(3); - expect(coordinates[0].lat).toBe(47); - expect(coordinates[1].lon).toBe(6.1); - expect(coordinates[2].lat).toBe(47.2); + const points: Point[] = postgresDirectionEncoder.decode(direction); + expect(points.length).toBe(3); + expect(points[0].lat).toBe(47); + expect(points[1].lon).toBe(6.1); + expect(points[2].lat).toBe(47.2); }); }); diff --git a/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts b/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts index 7e2de20..b4c4202 100644 --- a/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts +++ b/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts @@ -1,4 +1,3 @@ -import { Role } from '@modules/geography/core/domain/route.types'; import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller'; import { RouteMapper } from '@modules/geography/route.mapper'; import { QueryBus } from '@nestjs/cqrs'; @@ -48,7 +47,6 @@ describe('Get Basic Route Controller', () => { it('should get a route', async () => { jest.spyOn(mockQueryBus, 'execute'); await getBasicRouteController.get({ - roles: [Role.DRIVER], waypoints: [ { position: 0, diff --git a/src/modules/geography/tests/unit/route.mapper.spec.ts b/src/modules/geography/tests/unit/route.mapper.spec.ts index 7492a85..0d0ccf5 100644 --- a/src/modules/geography/tests/unit/route.mapper.spec.ts +++ b/src/modules/geography/tests/unit/route.mapper.spec.ts @@ -23,28 +23,23 @@ describe('Route Mapper', () => { createdAt: now, updatedAt: now, props: { - driverDistance: 23000, - driverDuration: 900, - passengerDistance: 23000, - passengerDuration: 900, + distance: 23000, + duration: 900, fwdAzimuth: 283, backAzimuth: 93, distanceAzimuth: 19840, - points: [], - waypoints: [ + points: [ { - position: 0, lon: 6.1765103, lat: 48.689446, }, { - position: 1, lon: 2.3523, lat: 48.8567, }, ], }, }); - expect(routeMapper.toResponse(routeEntity).driverDistance).toBe(23000); + expect(routeMapper.toResponse(routeEntity).distance).toBe(23000); }); }); From 6b4ac1792c15b32586c2a6defe33502468a0c418 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 7 Sep 2023 14:31:32 +0200 Subject: [PATCH 19/52] extract carpool informations from geography module --- .../infrastructure/carpool-route-provider.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/modules/ad/infrastructure/carpool-route-provider.ts b/src/modules/ad/infrastructure/carpool-route-provider.ts index 5cdb4dd..97e401e 100644 --- a/src/modules/ad/infrastructure/carpool-route-provider.ts +++ b/src/modules/ad/infrastructure/carpool-route-provider.ts @@ -18,7 +18,7 @@ export class CarpoolRouteProvider implements CarpoolRouteProviderPort { roles: Role[], waypoints: Waypoint[], ): Promise => { - const paths: Path[] = this.getPaths(roles, waypoints); + const paths: Path[] = this._getPaths(roles, waypoints); const typeRoutes: TypeRoute[] = await Promise.all( paths.map( async (path: Path) => @@ -77,39 +77,39 @@ export class CarpoolRouteProvider implements CarpoolRouteProviderPort { }; }; - private getPaths = (roles: Role[], waypoints: Waypoint[]): Path[] => { + private _getPaths = (roles: Role[], waypoints: Waypoint[]): Path[] => { const paths: Path[] = []; if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { if (waypoints.length == 2) { // 2 points => same route for driver and passenger - paths.push(this.createGenericPath(waypoints)); + paths.push(this._createGenericPath(waypoints)); } else { paths.push( - this.createDriverPath(waypoints), - this.createPassengerPath(waypoints), + this._createDriverPath(waypoints), + this._createPassengerPath(waypoints), ); } } else if (roles.includes(Role.DRIVER)) { - paths.push(this.createDriverPath(waypoints)); + paths.push(this._createDriverPath(waypoints)); } else if (roles.includes(Role.PASSENGER)) { - paths.push(this.createPassengerPath(waypoints)); + paths.push(this._createPassengerPath(waypoints)); } return paths; }; - private createGenericPath = (waypoints: Waypoint[]): Path => - this.createPath(waypoints, PathType.GENERIC); + private _createGenericPath = (waypoints: Waypoint[]): Path => + this._createPath(waypoints, PathType.GENERIC); - private createDriverPath = (waypoints: Waypoint[]): Path => - this.createPath(waypoints, PathType.DRIVER); + private _createDriverPath = (waypoints: Waypoint[]): Path => + this._createPath(waypoints, PathType.DRIVER); - private createPassengerPath = (waypoints: Waypoint[]): Path => - this.createPath( + private _createPassengerPath = (waypoints: Waypoint[]): Path => + this._createPath( [waypoints[0], waypoints[waypoints.length - 1]], PathType.PASSENGER, ); - private createPath = (waypoints: Waypoint[], type: PathType): Path => ({ + private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({ type, waypoints, }); From 59a2644bb4e15d7612ae33e39118025cbfe668bd Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 8 Sep 2023 16:19:25 +0200 Subject: [PATCH 20/52] simpler route provider in ad module --- src/modules/ad/ad.di-tokens.ts | 2 +- src/modules/ad/ad.module.ts | 8 +- .../commands/create-ad/create-ad.service.ts | 32 +-- .../ports/carpool-route-provider.port.ts | 10 - .../application/ports/route-provider.port.ts | 9 + .../queries/match/match.query-handler.ts | 10 +- .../application/queries/match/match.query.ts | 92 +++++-- .../selector/passenger-oriented.selector.ts | 11 +- .../core/application/types/algorithm.types.ts | 6 +- .../application/types/carpool-route.type.ts | 2 +- .../infrastructure/carpool-route-provider.ts | 132 ---------- .../ad/infrastructure/route-provider.ts | 19 ++ .../tests/unit/core/create-ad.service.spec.ts | 17 +- .../unit/core/match.query-handler.spec.ts | 17 +- .../ad/tests/unit/core/match.query.spec.ts | 128 +++++++++- .../passenger-oriented-geo-filter.spec.ts | 68 ++---- .../core/passenger-oriented-selector.spec.ts | 30 ++- ...enger-oriented-waypoints-completer.spec.ts | 68 ++---- .../unit/infrastructure/ad.repository.spec.ts | 8 +- .../carpool-route-provider.spec.ts | 230 ------------------ .../infrastructure/route-provider.spec.ts | 76 ++++++ .../geography/core/domain/route.types.ts | 2 +- .../interface/dtos/route.response.dto.ts | 7 +- src/modules/geography/route.mapper.ts | 8 +- .../graphhopper-georouter.spec.ts | 12 +- 25 files changed, 436 insertions(+), 568 deletions(-) delete mode 100644 src/modules/ad/core/application/ports/carpool-route-provider.port.ts create mode 100644 src/modules/ad/core/application/ports/route-provider.port.ts delete mode 100644 src/modules/ad/infrastructure/carpool-route-provider.ts create mode 100644 src/modules/ad/infrastructure/route-provider.ts delete mode 100644 src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 592fcc5..4a69ae2 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -4,7 +4,7 @@ export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER'); export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol( 'AD_GET_BASIC_ROUTE_CONTROLLER', ); -export const AD_CARPOOL_ROUTE_PROVIDER = Symbol('AD_CARPOOL_ROUTE_PROVIDER'); +export const AD_ROUTE_PROVIDER = Symbol('AD_ROUTE_PROVIDER'); export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index a1f9eb6..90610bd 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -4,7 +4,7 @@ import { AD_MESSAGE_PUBLISHER, AD_REPOSITORY, AD_DIRECTION_ENCODER, - AD_CARPOOL_ROUTE_PROVIDER, + AD_ROUTE_PROVIDER, AD_GET_BASIC_ROUTE_CONTROLLER, PARAMS_PROVIDER, TIMEZONE_FINDER, @@ -18,7 +18,7 @@ import { AdMapper } from './ad.mapper'; import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler'; import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; import { GetBasicRouteController } from '@modules/geography/interface/controllers/get-basic-route.controller'; -import { CarpoolRouteProvider } from './infrastructure/carpool-route-provider'; +import { RouteProvider } from './infrastructure/route-provider'; import { GeographyModule } from '@modules/geography/geography.module'; import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller'; @@ -60,8 +60,8 @@ const adapters: Provider[] = [ useClass: PostgresDirectionEncoder, }, { - provide: AD_CARPOOL_ROUTE_PROVIDER, - useClass: CarpoolRouteProvider, + provide: AD_ROUTE_PROVIDER, + useClass: RouteProvider, }, { provide: AD_GET_BASIC_ROUTE_CONTROLLER, diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index fb84626..c197801 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -1,35 +1,29 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CreateAdCommand } from './create-ad.command'; import { Inject } from '@nestjs/common'; -import { - AD_REPOSITORY, - AD_CARPOOL_ROUTE_PROVIDER, -} from '@modules/ad/ad.di-tokens'; +import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; -import { CarpoolRouteProviderPort } from '../../ports/carpool-route-provider.port'; +import { RouteProviderPort } from '../../ports/route-provider.port'; import { Role } from '@modules/ad/core/domain/ad.types'; -import { CarpoolRoute } from '../../types/carpool-route.type'; +import { Route } from '../../types/carpool-route.type'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { constructor( @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, - @Inject(AD_CARPOOL_ROUTE_PROVIDER) - private readonly carpoolRouteProvider: CarpoolRouteProviderPort, + @Inject(AD_ROUTE_PROVIDER) + private readonly routeProvider: RouteProviderPort, ) {} async execute(command: CreateAdCommand): Promise { const roles: Role[] = []; if (command.driver) roles.push(Role.DRIVER); if (command.passenger) roles.push(Role.PASSENGER); - const carpoolRoute: CarpoolRoute = await this.carpoolRouteProvider.getBasic( - roles, - command.waypoints, - ); + const route: Route = await this.routeProvider.getBasic(command.waypoints); const ad = AdEntity.create({ id: command.id, driver: command.driver, @@ -42,13 +36,13 @@ export class CreateAdService implements ICommandHandler { seatsRequested: command.seatsRequested, strict: command.strict, waypoints: command.waypoints, - points: carpoolRoute.points, - driverDistance: carpoolRoute.driverDistance, - driverDuration: carpoolRoute.driverDuration, - passengerDistance: carpoolRoute.passengerDistance, - passengerDuration: carpoolRoute.passengerDuration, - fwdAzimuth: carpoolRoute.fwdAzimuth, - backAzimuth: carpoolRoute.backAzimuth, + points: route.points, + driverDistance: route.driverDistance, + driverDuration: route.driverDuration, + passengerDistance: route.passengerDistance, + passengerDuration: route.passengerDuration, + fwdAzimuth: route.fwdAzimuth, + backAzimuth: route.backAzimuth, }); try { diff --git a/src/modules/ad/core/application/ports/carpool-route-provider.port.ts b/src/modules/ad/core/application/ports/carpool-route-provider.port.ts deleted file mode 100644 index 24b7f5a..0000000 --- a/src/modules/ad/core/application/ports/carpool-route-provider.port.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Role } from '../../domain/ad.types'; -import { Waypoint } from '../types/waypoint.type'; -import { CarpoolRoute } from '../types/carpool-route.type'; - -export interface CarpoolRouteProviderPort { - /** - * Get a basic carpool route with points and overall duration / distance - */ - getBasic(roles: Role[], waypoints: Waypoint[]): Promise; -} diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts new file mode 100644 index 0000000..7974886 --- /dev/null +++ b/src/modules/ad/core/application/ports/route-provider.port.ts @@ -0,0 +1,9 @@ +import { Route } from '@modules/geography/core/domain/route.types'; +import { Waypoint } from '../types/waypoint.type'; + +export interface RouteProviderPort { + /** + * Get a basic route with points and overall duration / distance + */ + getBasic(waypoints: Waypoint[]): Promise; +} diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 7510a5f..14a8af5 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -7,7 +7,7 @@ import { Inject } from '@nestjs/common'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; import { AD_REPOSITORY, - AD_CARPOOL_ROUTE_PROVIDER, + AD_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; @@ -15,7 +15,7 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port'; import { DefaultParams } from '../../ports/default-params.type'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; -import { CarpoolRouteProviderPort } from '../../ports/carpool-route-provider.port'; +import { RouteProviderPort } from '../../ports/route-provider.port'; @QueryHandler(MatchQuery) export class MatchQueryHandler implements IQueryHandler { @@ -27,8 +27,8 @@ export class MatchQueryHandler implements IQueryHandler { @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, - @Inject(AD_CARPOOL_ROUTE_PROVIDER) - private readonly carpoolRouteProvider: CarpoolRouteProviderPort, + @Inject(AD_ROUTE_PROVIDER) + private readonly routeProvider: RouteProviderPort, ) { this._defaultParams = defaultParamsProvider.getParams(); } @@ -54,7 +54,7 @@ export class MatchQueryHandler implements IQueryHandler { maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, }) .setDatesAndSchedule(this.datetimeTransformer); - await query.setCarpoolRoute(this.carpoolRouteProvider); + await query.setRoutes(this.routeProvider); let algorithm: Algorithm; switch (query.algorithmType) { diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index a329da9..1b0c559 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -1,11 +1,11 @@ import { QueryBase } from '@mobicoop/ddd-library'; import { AlgorithmType } from '../../types/algorithm.types'; import { Waypoint } from '../../types/waypoint.type'; -import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; -import { CarpoolRoute } from '../../types/carpool-route.type'; -import { CarpoolRouteProviderPort } from '../../ports/carpool-route-provider.port'; +import { RouteProviderPort } from '../../ports/route-provider.port'; +import { Route } from '@modules/geography/core/domain/route.types'; export class MatchQuery extends QueryBase { driver?: boolean; @@ -28,7 +28,9 @@ export class MatchQuery extends QueryBase { maxDetourDurationRatio?: number; readonly page?: number; readonly perPage?: number; - carpoolRoute?: CarpoolRoute; + driverRoute?: Route; + passengerRoute?: Route; + backAzimuth?: number; constructor(props: MatchRequestDto) { super(); @@ -165,22 +167,68 @@ export class MatchQuery extends QueryBase { return this; }; - setCarpoolRoute = async ( - carpoolRouteProvider: CarpoolRouteProviderPort, - ): Promise => { - const roles: Role[] = []; - if (this.driver) roles.push(Role.DRIVER); - if (this.passenger) roles.push(Role.PASSENGER); + setRoutes = async (routeProvider: RouteProviderPort): Promise => { try { - this.carpoolRoute = await carpoolRouteProvider.getBasic( - roles, - this.waypoints, - ); + ( + await Promise.all( + this._getPaths().map(async (path: Path) => ({ + type: path.type, + route: await routeProvider.getBasic(path.waypoints), + })), + ) + ).forEach((typedRoute: TypedRoute) => { + if (typedRoute.type !== PathType.PASSENGER) { + this.driverRoute = typedRoute.route; + this.backAzimuth = typedRoute.route.backAzimuth; + } + if (typedRoute.type !== PathType.DRIVER) { + this.passengerRoute = typedRoute.route; + if (!this.backAzimuth) + this.backAzimuth = typedRoute.route.backAzimuth; + } + }); } catch (e: any) { throw new Error('Unable to find a route for given waypoints'); } return this; }; + + private _getPaths = (): Path[] => { + const paths: Path[] = []; + if (this.driver && this.passenger) { + if (this.waypoints.length == 2) { + // 2 points => same route for driver and passenger + paths.push(this._createGenericPath(this.waypoints)); + } else { + paths.push( + this._createDriverPath(this.waypoints), + this._createPassengerPath(this.waypoints), + ); + } + } else if (this.driver) { + paths.push(this._createDriverPath(this.waypoints)); + } else if (this.passenger) { + paths.push(this._createPassengerPath(this.waypoints)); + } + return paths; + }; + + private _createGenericPath = (waypoints: Waypoint[]): Path => + this._createPath(waypoints, PathType.GENERIC); + + private _createDriverPath = (waypoints: Waypoint[]): Path => + this._createPath(waypoints, PathType.DRIVER); + + private _createPassengerPath = (waypoints: Waypoint[]): Path => + this._createPath( + [waypoints[0], waypoints[waypoints.length - 1]], + PathType.PASSENGER, + ); + + private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({ + type, + waypoints, + }); } export type ScheduleItem = { @@ -206,3 +254,19 @@ interface DefaultAlgorithmParameters { maxDetourDistanceRatio: number; maxDetourDurationRatio: number; } + +type Path = { + type: PathType; + waypoints: Waypoint[]; +}; + +enum PathType { + GENERIC = 'generic', + DRIVER = 'driver', + PASSENGER = 'passenger', +} + +type TypedRoute = { + type: PathType; + route: Route; +}; diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 5c828ea..55a394d 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -39,7 +39,6 @@ export class PassengerOrientedSelector extends Selector { id: adReadModel.uuid, }, role: adsRole.role, - baseCarpoolRoute: this.query.carpoolRoute, }, ), ) @@ -69,7 +68,7 @@ export class PassengerOrientedSelector extends Selector { ].join(); private _selectAsDriver = (): string => - `${this.query.carpoolRoute?.driverDuration} as duration,${this.query.carpoolRoute?.driverDistance} as distance`; + `${this.query.driverRoute?.duration} as duration,${this.query.driverRoute?.distance} as distance`; private _selectAsPassenger = (): string => `"driverDuration" as duration,"driverDistance" as distance`; @@ -200,7 +199,7 @@ export class PassengerOrientedSelector extends Selector { private _whereAzimuth = (): string => { if (!this.query.useAzimuth) return ''; const { minAzimuth, maxAzimuth } = this._azimuthRange( - this.query.carpoolRoute?.backAzimuth as number, + this.query.backAzimuth as number, this.query.azimuthMargin as number, ); if (minAzimuth <= maxAzimuth) @@ -212,9 +211,9 @@ export class PassengerOrientedSelector extends Selector { if (!this.query.useProportion) return ''; switch (role) { case Role.PASSENGER: - return `(${this.query.carpoolRoute?.passengerDistance}>(${this.query.proportion}*"driverDistance"))`; + return `(${this.query.passengerRoute?.distance}>(${this.query.proportion}*"driverDistance"))`; case Role.DRIVER: - return `("passengerDistance">(${this.query.proportion}*${this.query.carpoolRoute?.driverDistance}))`; + return `("passengerDistance">(${this.query.proportion}*${this.query.driverRoute?.distance}))`; } }; @@ -238,7 +237,7 @@ export class PassengerOrientedSelector extends Selector { ${this.query.remoteness}`; case Role.DRIVER: const lineStringPoints: string[] = []; - this.query.carpoolRoute?.points.forEach((point: Point) => + this.query.driverRoute?.points.forEach((point: Point) => lineStringPoints.push( `public.st_makepoint(${point.lon},${point.lat})`, ), diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts index 83cc2aa..d26b41d 100644 --- a/src/modules/ad/core/application/types/algorithm.types.ts +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -1,5 +1,5 @@ +import { Waypoint } from '@modules/geography/core/domain/route.types'; import { Role } from '../../domain/ad.types'; -import { CarpoolRoute } from './carpool-route.type'; export enum AlgorithmType { PASSENGER_ORIENTED = 'PASSENGER_ORIENTED', @@ -11,9 +11,7 @@ export enum AlgorithmType { export type Candidate = { ad: Ad; role: Role; - baseCarpoolRoute: CarpoolRoute; - // driverRoute?: Route; ? - // crewRoute?: Route; ? + waypoints: Waypoint[]; }; export type Ad = { diff --git a/src/modules/ad/core/application/types/carpool-route.type.ts b/src/modules/ad/core/application/types/carpool-route.type.ts index b219701..7b23bfb 100644 --- a/src/modules/ad/core/application/types/carpool-route.type.ts +++ b/src/modules/ad/core/application/types/carpool-route.type.ts @@ -3,7 +3,7 @@ import { Point } from './point.type'; /** * A carpool route is a route with distance and duration as driver and / or passenger */ -export type CarpoolRoute = { +export type Route = { driverDistance?: number; driverDuration?: number; passengerDistance?: number; diff --git a/src/modules/ad/infrastructure/carpool-route-provider.ts b/src/modules/ad/infrastructure/carpool-route-provider.ts deleted file mode 100644 index 97e401e..0000000 --- a/src/modules/ad/infrastructure/carpool-route-provider.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { CarpoolRouteProviderPort } from '../core/application/ports/carpool-route-provider.port'; -import { CarpoolRoute } from '../core/application/types/carpool-route.type'; -import { Waypoint } from '../core/application/types/waypoint.type'; -import { Role } from '../core/domain/ad.types'; -import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; -import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; -import { Route } from '@modules/geography/core/domain/route.types'; - -@Injectable() -export class CarpoolRouteProvider implements CarpoolRouteProviderPort { - constructor( - @Inject(AD_GET_BASIC_ROUTE_CONTROLLER) - private readonly getBasicRouteController: GetBasicRouteControllerPort, - ) {} - - getBasic = async ( - roles: Role[], - waypoints: Waypoint[], - ): Promise => { - const paths: Path[] = this._getPaths(roles, waypoints); - const typeRoutes: TypeRoute[] = await Promise.all( - paths.map( - async (path: Path) => - { - type: path.type, - route: await this.getBasicRouteController.get({ - waypoints, - }), - }, - ), - ); - return this._toCarpoolRoute(typeRoutes); - }; - - private _toCarpoolRoute = (typeRoutes: TypeRoute[]): CarpoolRoute => { - let baseRoute: Route; - let driverRoute: Route | undefined; - let passengerRoute: Route | undefined; - if ( - typeRoutes.some( - (typeRoute: TypeRoute) => typeRoute.type == PathType.GENERIC, - ) - ) { - driverRoute = passengerRoute = typeRoutes.find( - (typeRoute: TypeRoute) => typeRoute.type == PathType.GENERIC, - )?.route; - } else { - driverRoute = typeRoutes.some( - (typeRoute: TypeRoute) => typeRoute.type == PathType.DRIVER, - ) - ? typeRoutes.find( - (typeRoute: TypeRoute) => typeRoute.type == PathType.DRIVER, - )?.route - : undefined; - passengerRoute = typeRoutes.some( - (typeRoute: TypeRoute) => typeRoute.type == PathType.PASSENGER, - ) - ? typeRoutes.find( - (typeRoute: TypeRoute) => typeRoute.type == PathType.PASSENGER, - )?.route - : undefined; - } - if (driverRoute) { - baseRoute = driverRoute; - } else { - baseRoute = passengerRoute as Route; - } - return { - driverDistance: driverRoute?.distance, - driverDuration: driverRoute?.duration, - passengerDistance: passengerRoute?.distance, - passengerDuration: passengerRoute?.duration, - fwdAzimuth: baseRoute.fwdAzimuth, - backAzimuth: baseRoute.backAzimuth, - points: baseRoute.points, - }; - }; - - private _getPaths = (roles: Role[], waypoints: Waypoint[]): Path[] => { - const paths: Path[] = []; - if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { - if (waypoints.length == 2) { - // 2 points => same route for driver and passenger - paths.push(this._createGenericPath(waypoints)); - } else { - paths.push( - this._createDriverPath(waypoints), - this._createPassengerPath(waypoints), - ); - } - } else if (roles.includes(Role.DRIVER)) { - paths.push(this._createDriverPath(waypoints)); - } else if (roles.includes(Role.PASSENGER)) { - paths.push(this._createPassengerPath(waypoints)); - } - return paths; - }; - - private _createGenericPath = (waypoints: Waypoint[]): Path => - this._createPath(waypoints, PathType.GENERIC); - - private _createDriverPath = (waypoints: Waypoint[]): Path => - this._createPath(waypoints, PathType.DRIVER); - - private _createPassengerPath = (waypoints: Waypoint[]): Path => - this._createPath( - [waypoints[0], waypoints[waypoints.length - 1]], - PathType.PASSENGER, - ); - - private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({ - type, - waypoints, - }); -} - -type Path = { - type: PathType; - waypoints: Waypoint[]; -}; - -type TypeRoute = { - type: PathType; - route: Route; -}; - -enum PathType { - GENERIC = 'generic', - DRIVER = 'driver', - PASSENGER = 'passenger', -} diff --git a/src/modules/ad/infrastructure/route-provider.ts b/src/modules/ad/infrastructure/route-provider.ts new file mode 100644 index 0000000..52a0189 --- /dev/null +++ b/src/modules/ad/infrastructure/route-provider.ts @@ -0,0 +1,19 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { RouteProviderPort } from '../core/application/ports/route-provider.port'; +import { Waypoint } from '../core/application/types/waypoint.type'; +import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; +import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; +import { Route } from '@modules/geography/core/domain/route.types'; + +@Injectable() +export class RouteProvider implements RouteProviderPort { + constructor( + @Inject(AD_GET_BASIC_ROUTE_CONTROLLER) + private readonly getBasicRouteController: GetBasicRouteControllerPort, + ) {} + + getBasic = async (waypoints: Waypoint[]): Promise => + await this.getBasicRouteController.get({ + waypoints, + }); +} diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 9f53e98..d0cfb3e 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -1,8 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { - AD_REPOSITORY, - AD_CARPOOL_ROUTE_PROVIDER, -} from '@modules/ad/ad.di-tokens'; +import { AD_REPOSITORY, AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { AggregateID } from '@mobicoop/ddd-library'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { ConflictException } from '@mobicoop/ddd-library'; @@ -11,7 +8,7 @@ import { CreateAdService } from '@modules/ad/core/application/commands/create-ad import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; -import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; const originWaypoint: WaypointProps = { position: 0, @@ -62,12 +59,10 @@ const mockAdRepository = { }), }; -const mockRouteProvider: CarpoolRouteProviderPort = { +const mockRouteProvider: RouteProviderPort = { getBasic: jest.fn().mockImplementation(() => ({ - driverDistance: 350101, - driverDuration: 14422, - passengerDistance: 350101, - passengerDuration: 14422, + distance: 350101, + duration: 14422, fwdAzimuth: 273, backAzimuth: 93, distanceAzimuth: 336544, @@ -99,7 +94,7 @@ describe('create-ad.service', () => { useValue: mockAdRepository, }, { - provide: AD_CARPOOL_ROUTE_PROVIDER, + provide: AD_ROUTE_PROVIDER, useValue: mockRouteProvider, }, CreateAdService, diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 25b7a8a..275b97f 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,12 +1,12 @@ import { AD_REPOSITORY, - AD_CARPOOL_ROUTE_PROVIDER, + AD_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; -import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; @@ -74,8 +74,15 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = { time: jest.fn(), }; -const mockRouteProvider: CarpoolRouteProviderPort = { - getBasic: jest.fn(), +const mockRouteProvider: RouteProviderPort = { + getBasic: jest.fn().mockImplementation(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })), }; describe('Match Query Handler', () => { @@ -98,7 +105,7 @@ describe('Match Query Handler', () => { useValue: mockInputDateTimeTransformer, }, { - provide: AD_CARPOOL_ROUTE_PROVIDER, + provide: AD_ROUTE_PROVIDER, useValue: mockRouteProvider, }, ], diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts index 3c8c4d5..17440de 100644 --- a/src/modules/ad/tests/unit/core/match.query.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -1,6 +1,6 @@ import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type'; -import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; @@ -24,6 +24,14 @@ const destinationWaypoint: Waypoint = { postalCode: '75000', country: 'France', }; +const intermediateWaypoint: Waypoint = { + position: 1, + lat: 48.966912, + lon: 4.3655, + locality: 'Châlons-en-Champagne', + postalCode: '51000', + country: 'France', +}; const defaultParams: DefaultParams = { DEPARTURE_TIME_MARGIN: 900, @@ -50,16 +58,47 @@ const mockInputDateTimeTransformer: DateTimeTransformerPort = { time: jest.fn().mockImplementation(() => '23:05'), }; -const mockRouteProvider: CarpoolRouteProviderPort = { +const mockRouteProvider: RouteProviderPort = { getBasic: jest .fn() .mockImplementationOnce(() => ({ - driverDistance: undefined, - driverDuration: undefined, - passengerDistance: 150120, - passengerDuration: 6540, - fwdAzimuth: 276, - backAzimuth: 96, + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })) + .mockImplementationOnce(() => ({ + distance: 340102, + duration: 13423, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })) + .mockImplementationOnce(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })) + .mockImplementationOnce(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })) + .mockImplementationOnce(() => ({ + distance: 340102, + duration: 13423, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, points: [], })) .mockImplementationOnce(() => { @@ -143,8 +182,10 @@ describe('Match Query', () => { expect(matchQuery.seatsRequested).toBe(1); }); - it('should set route', async () => { + it('should set route for a driver only', async () => { const matchQuery = new MatchQuery({ + driver: true, + passenger: false, frequency: Frequency.PUNCTUAL, fromDate: '2023-08-28', toDate: '2023-08-28', @@ -155,9 +196,70 @@ describe('Match Query', () => { ], waypoints: [originWaypoint, destinationWaypoint], }); - await matchQuery.setCarpoolRoute(mockRouteProvider); - expect(matchQuery.carpoolRoute?.driverDistance).toBeUndefined(); - expect(matchQuery.carpoolRoute?.passengerDistance).toBe(150120); + await matchQuery.setRoutes(mockRouteProvider); + expect(matchQuery.driverRoute?.distance).toBe(350101); + expect(matchQuery.passengerRoute).toBeUndefined(); + }); + + it('should set route for a passenger only', async () => { + const matchQuery = new MatchQuery({ + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }); + await matchQuery.setRoutes(mockRouteProvider); + expect(matchQuery.passengerRoute?.distance).toBe(340102); + expect(matchQuery.driverRoute).toBeUndefined(); + }); + + it('should set route for a driver and passenger', async () => { + const matchQuery = new MatchQuery({ + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }); + await matchQuery.setRoutes(mockRouteProvider); + expect(matchQuery.driverRoute?.distance).toBe(350101); + expect(matchQuery.passengerRoute?.distance).toBe(350101); + }); + + it('should set route for a driver and passenger with 3 waypoints', async () => { + const matchQuery = new MatchQuery({ + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [ + originWaypoint, + intermediateWaypoint, + { ...destinationWaypoint, position: 2 }, + ], + }); + await matchQuery.setRoutes(mockRouteProvider); + expect(matchQuery.driverRoute?.distance).toBe(350101); + expect(matchQuery.passengerRoute?.distance).toBe(340102); }); it('should throw an exception if route is not found', async () => { @@ -175,7 +277,7 @@ describe('Match Query', () => { waypoints: [originWaypoint, destinationWaypoint], }); await expect( - matchQuery.setCarpoolRoute(mockRouteProvider), + matchQuery.setRoutes(mockRouteProvider), ).rejects.toBeInstanceOf(Error); }); }); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 9d34a9f..43f9533 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -48,56 +48,36 @@ const candidates: Candidate[] = [ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', }, role: Role.DRIVER, - baseCarpoolRoute: { - driverDistance: 350101, - driverDuration: 14422, - passengerDistance: 350101, - passengerDuration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - }, + waypoints: [ + { + position: 0, + lat: 48.68874, + lon: 6.18546, + }, + { + position: 1, + lat: 48.87845, + lon: 2.36547, + }, + ], }, { ad: { id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', }, role: Role.PASSENGER, - baseCarpoolRoute: { - driverDistance: 350101, - driverDuration: 14422, - passengerDistance: 350101, - passengerDuration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - }, + waypoints: [ + { + position: 0, + lat: 48.69844, + lon: 6.168484, + }, + { + position: 1, + lat: 48.855648, + lon: 2.34645, + }, + ], }, ]; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index 1b9860d..85911f1 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -48,13 +48,33 @@ const matchQuery = new MatchQuery({ strict: false, waypoints: [originWaypoint, destinationWaypoint], }); -matchQuery.carpoolRoute = { - driverDistance: 150120, - driverDuration: 6540, - passengerDistance: 150120, - passengerDuration: 6540, +matchQuery.driverRoute = { + distance: 150120, + duration: 6540, fwdAzimuth: 276, backAzimuth: 96, + distanceAzimuth: 148321, + points: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.7566, + lon: 4.3522, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], +}; +matchQuery.passengerRoute = { + distance: 150120, + duration: 6540, + fwdAzimuth: 276, + backAzimuth: 96, + distanceAzimuth: 148321, points: [ { lat: 48.689445, diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts index 5ca81c3..43d898b 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts @@ -48,56 +48,36 @@ const candidates: Candidate[] = [ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', }, role: Role.DRIVER, - baseCarpoolRoute: { - driverDistance: 350101, - driverDuration: 14422, - passengerDistance: 350101, - passengerDuration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - }, + waypoints: [ + { + position: 0, + lat: 48.69, + lon: 6.18, + }, + { + position: 1, + lat: 48.87, + lon: 2.37, + }, + ], }, { ad: { id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', }, role: Role.PASSENGER, - baseCarpoolRoute: { - driverDistance: 350101, - driverDuration: 14422, - passengerDistance: 350101, - passengerDuration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - }, + waypoints: [ + { + position: 0, + lat: 48.63584, + lon: 6.148754, + }, + { + position: 1, + lat: 48.89874, + lon: 2.368745, + }, + ], }, ]; diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index ce217c6..1ba6e92 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -1,10 +1,10 @@ import { AD_DIRECTION_ENCODER, AD_MESSAGE_PUBLISHER, - AD_CARPOOL_ROUTE_PROVIDER, + AD_ROUTE_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; -import { CarpoolRouteProviderPort } from '@modules/ad/core/application/ports/carpool-route-provider.port'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AdReadModel, @@ -24,7 +24,7 @@ const mockDirectionEncoder: DirectionEncoderPort = { decode: jest.fn(), }; -const mockRouteProvider: CarpoolRouteProviderPort = { +const mockRouteProvider: RouteProviderPort = { getBasic: jest.fn(), }; @@ -173,7 +173,7 @@ describe('Ad repository', () => { useValue: mockDirectionEncoder, }, { - provide: AD_CARPOOL_ROUTE_PROVIDER, + provide: AD_ROUTE_PROVIDER, useValue: mockRouteProvider, }, { diff --git a/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts deleted file mode 100644 index 3e96571..0000000 --- a/src/modules/ad/tests/unit/infrastructure/carpool-route-provider.spec.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens'; -import { CarpoolRoute } from '@modules/ad/core/application/types/carpool-route.type'; -import { Point } from '@modules/ad/core/application/types/point.type'; -import { Role } from '@modules/ad/core/domain/ad.types'; -import { CarpoolRouteProvider } from '@modules/ad/infrastructure/carpool-route-provider'; -import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; -import { Test, TestingModule } from '@nestjs/testing'; - -const originPoint: Point = { - lat: 48.689445, - lon: 6.17651, -}; -const destinationPoint: Point = { - lat: 48.8566, - lon: 2.3522, -}; -const additionalPoint: Point = { - lon: 48.7566, - lat: 4.4498, -}; - -const mockGetBasicRouteController: GetBasicRouteControllerPort = { - get: jest - .fn() - .mockImplementationOnce(() => ({ - distance: 350101, - duration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336544, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - })) - .mockImplementationOnce(() => ({ - distance: 350102, - duration: 14423, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336545, - points: [ - { - lon: 6.1765103, - lat: 48.689446, - }, - { - lon: 4.984579, - lat: 48.725688, - }, - { - lon: 2.3523, - lat: 48.8567, - }, - ], - })) - .mockImplementationOnce(() => ({ - distance: 350100, - duration: 14421, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336543, - points: [ - { - lon: 6.1765101, - lat: 48.689444, - }, - { - lon: 4.984577, - lat: 48.725686, - }, - { - lon: 2.3521, - lat: 48.8565, - }, - ], - })) - .mockImplementationOnce(() => ({ - distance: 350107, - duration: 14427, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336548, - points: [ - { - lon: 6.1765101, - lat: 48.689444, - }, - { - lon: 4.984577, - lat: 48.725686, - }, - { - lon: 2.3521, - lat: 48.8565, - }, - ], - })) - .mockImplementationOnce(() => ({ - distance: 350108, - duration: 14428, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336548, - points: [ - { - lon: 6.1765101, - lat: 48.689444, - }, - { - lon: 4.984577, - lat: 48.725686, - }, - { - lon: 2.3521, - lat: 48.8565, - }, - ], - })) - .mockImplementationOnce(() => []), -}; - -describe('Carpool route provider', () => { - let carpoolRouteProvider: CarpoolRouteProvider; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CarpoolRouteProvider, - { - provide: AD_GET_BASIC_ROUTE_CONTROLLER, - useValue: mockGetBasicRouteController, - }, - ], - }).compile(); - - carpoolRouteProvider = - module.get(CarpoolRouteProvider); - }); - - it('should be defined', () => { - expect(carpoolRouteProvider).toBeDefined(); - }); - - it('should provide a carpool route for a driver only', async () => { - const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( - [Role.DRIVER], - [ - { - position: 0, - ...originPoint, - }, - { - position: 1, - ...destinationPoint, - }, - ], - ); - expect(carpoolRoute.driverDistance).toBe(350101); - expect(carpoolRoute.passengerDuration).toBeUndefined(); - }); - - it('should provide a carpool route for a passenger only', async () => { - const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( - [Role.PASSENGER], - [ - { - position: 0, - ...originPoint, - }, - { - position: 1, - ...destinationPoint, - }, - ], - ); - expect(carpoolRoute.passengerDistance).toBe(350102); - expect(carpoolRoute.driverDuration).toBeUndefined(); - }); - - it('should provide a simple carpool route for a driver and passenger', async () => { - const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( - [Role.DRIVER, Role.PASSENGER], - [ - { - position: 0, - ...originPoint, - }, - { - position: 1, - ...destinationPoint, - }, - ], - ); - expect(carpoolRoute.driverDuration).toBe(14421); - expect(carpoolRoute.passengerDistance).toBe(350100); - }); - - it('should provide a complex carpool route for a driver and passenger', async () => { - const carpoolRoute: CarpoolRoute = await carpoolRouteProvider.getBasic( - [Role.DRIVER, Role.PASSENGER], - [ - { - position: 0, - ...originPoint, - }, - { - position: 1, - ...additionalPoint, - }, - { - position: 2, - ...destinationPoint, - }, - ], - ); - expect(carpoolRoute.driverDistance).toBe(350107); - expect(carpoolRoute.passengerDuration).toBe(14428); - }); -}); diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts new file mode 100644 index 0000000..4791b9b --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts @@ -0,0 +1,76 @@ +import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens'; +import { Point } from '@modules/ad/core/application/types/point.type'; +import { RouteProvider } from '@modules/ad/infrastructure/route-provider'; +import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; +import { Route } from '@modules/geography/core/domain/route.types'; +import { Test, TestingModule } from '@nestjs/testing'; + +const originPoint: Point = { + lat: 48.689445, + lon: 6.17651, +}; +const destinationPoint: Point = { + lat: 48.8566, + lon: 2.3522, +}; + +const mockGetBasicRouteController: GetBasicRouteControllerPort = { + get: jest.fn().mockImplementationOnce(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + })), +}; + +describe('Route provider', () => { + let routeProvider: RouteProvider; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RouteProvider, + { + provide: AD_GET_BASIC_ROUTE_CONTROLLER, + useValue: mockGetBasicRouteController, + }, + ], + }).compile(); + + routeProvider = module.get(RouteProvider); + }); + + it('should be defined', () => { + expect(routeProvider).toBeDefined(); + }); + + it('should provide a route', async () => { + const route: Route = await routeProvider.getBasic([ + { + position: 0, + ...originPoint, + }, + { + position: 1, + ...destinationPoint, + }, + ]); + expect(route.distance).toBe(350101); + expect(route.duration).toBe(14422); + }); +}); diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index 9340718..43855bc 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -27,7 +27,7 @@ export type Route = { backAzimuth: number; distanceAzimuth: number; points: Point[]; - steps: Step[]; + steps?: Step[]; }; export type Point = { diff --git a/src/modules/geography/interface/dtos/route.response.dto.ts b/src/modules/geography/interface/dtos/route.response.dto.ts index c157585..21d2ec1 100644 --- a/src/modules/geography/interface/dtos/route.response.dto.ts +++ b/src/modules/geography/interface/dtos/route.response.dto.ts @@ -1,10 +1,11 @@ import { Point, Step } from '@modules/geography/core/domain/route.types'; export class RouteResponseDto { - distance?: number; - duration?: number; + distance: number; + duration: number; fwdAzimuth: number; backAzimuth: number; distanceAzimuth: number; - points: Step[] | Point[]; + points: Point[]; + steps?: Step[]; } diff --git a/src/modules/geography/route.mapper.ts b/src/modules/geography/route.mapper.ts index c7d6eef..6353bad 100644 --- a/src/modules/geography/route.mapper.ts +++ b/src/modules/geography/route.mapper.ts @@ -16,12 +16,8 @@ export class RouteMapper { toResponse = (entity: RouteEntity): RouteResponseDto => { const response = new RouteResponseDto(); - response.distance = entity.getProps().distance - ? Math.round(entity.getProps().distance as number) - : undefined; - response.duration = entity.getProps().duration - ? Math.round(entity.getProps().duration as number) - : undefined; + response.distance = Math.round(entity.getProps().distance); + response.duration = Math.round(entity.getProps().duration); response.fwdAzimuth = Math.round(entity.getProps().fwdAzimuth); response.backAzimuth = Math.round(entity.getProps().backAzimuth); response.distanceAzimuth = Math.round(entity.getProps().distanceAzimuth); diff --git a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts index 1a40b5d..9686644 100644 --- a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts @@ -4,7 +4,7 @@ import { GeorouterUnavailableException, RouteNotFoundException, } from '@modules/geography/core/domain/route.errors'; -import { Route } from '@modules/geography/core/domain/route.types'; +import { Route, Step } from '@modules/geography/core/domain/route.types'; import { GEODESIC, PARAMS_PROVIDER, @@ -411,8 +411,8 @@ describe('Graphhopper Georouter', () => { }, ); expect(route.steps).toHaveLength(2); - expect(route.steps[1].duration).toBe(1800); - expect(route.steps[1].distance).toBeUndefined(); + expect((route.steps as Step[])[1].duration).toBe(1800); + expect((route.steps as Step[])[1].distance).toBeUndefined(); }); it('should create one route with points and missed waypoints extrapolations', async () => { @@ -468,8 +468,8 @@ describe('Graphhopper Georouter', () => { points: true, }, ); - expect(route.steps.length).toBe(3); - expect(route.steps[1].duration).toBe(990); - expect(route.steps[1].distance).toBe(25000); + expect(route.steps).toHaveLength(3); + expect((route.steps as Step[])[1].duration).toBe(990); + expect((route.steps as Step[])[1].distance).toBe(25000); }); }); From 2058bfce4cdd8749511d27edf753840abcb49175 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 11 Sep 2023 12:34:31 +0200 Subject: [PATCH 21/52] extract path creator --- .../commands/create-ad/create-ad.service.ts | 103 +++++++++++++----- .../queries/match/algorithm.abstract.ts | 13 ++- .../match/completer/completer.abstract.ts | 6 +- .../passenger-oriented-waypoints.completer.ts | 19 +++- .../queries/match/filter/filter.abstract.ts | 6 +- .../filter/passenger-oriented-geo.filter.ts | 5 +- .../application/queries/match/match.query.ts | 67 ++---------- .../selector/passenger-oriented.selector.ts | 17 ++- .../core/application/types/algorithm.types.ts | 3 +- .../application/types/carpool-route.type.ts | 14 --- .../ad/core/domain/candidate.entity.ts | 15 +++ src/modules/ad/core/domain/candidate.types.ts | 18 +++ .../ad/core/domain/patch-creator.service.ts | 77 +++++++++++++ .../tests/unit/core/create-ad.service.spec.ts | 71 +++++++----- .../passenger-oriented-geo-filter.spec.ts | 50 ++------- .../core/passenger-oriented-selector.spec.ts | 9 +- ...enger-oriented-waypoints-completer.spec.ts | 50 ++------- .../unit/core/path-creator.service.spec.ts | 78 +++++++++++++ 18 files changed, 382 insertions(+), 239 deletions(-) delete mode 100644 src/modules/ad/core/application/types/carpool-route.type.ts create mode 100644 src/modules/ad/core/domain/candidate.entity.ts create mode 100644 src/modules/ad/core/domain/candidate.types.ts create mode 100644 src/modules/ad/core/domain/patch-creator.service.ts create mode 100644 src/modules/ad/tests/unit/core/path-creator.service.spec.ts diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index c197801..55c2788 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -8,7 +8,13 @@ import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; import { RouteProviderPort } from '../../ports/route-provider.port'; import { Role } from '@modules/ad/core/domain/ad.types'; -import { Route } from '../../types/carpool-route.type'; +import { + Path, + PathCreator, + PathType, + TypedRoute, +} from '@modules/ad/core/domain/patch-creator.service'; +import { Point } from '../../types/point.type'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { @@ -23,36 +29,73 @@ export class CreateAdService implements ICommandHandler { const roles: Role[] = []; if (command.driver) roles.push(Role.DRIVER); if (command.passenger) roles.push(Role.PASSENGER); - const route: Route = await this.routeProvider.getBasic(command.waypoints); - const ad = AdEntity.create({ - id: command.id, - driver: command.driver, - passenger: command.passenger, - frequency: command.frequency, - fromDate: command.fromDate, - toDate: command.toDate, - schedule: command.schedule, - seatsProposed: command.seatsProposed, - seatsRequested: command.seatsRequested, - strict: command.strict, - waypoints: command.waypoints, - points: route.points, - driverDistance: route.driverDistance, - driverDuration: route.driverDuration, - passengerDistance: route.passengerDistance, - passengerDuration: route.passengerDuration, - fwdAzimuth: route.fwdAzimuth, - backAzimuth: route.backAzimuth, - }); - + const pathCreator: PathCreator = new PathCreator(roles, command.waypoints); + let typedRoutes: TypedRoute[]; try { - await this.repository.insertExtra(ad, 'ad'); - return ad.id; - } catch (error: any) { - if (error instanceof ConflictException) { - throw new AdAlreadyExistsException(error); - } - throw error; + typedRoutes = await Promise.all( + pathCreator.getPaths().map(async (path: Path) => ({ + type: path.type, + route: await this.routeProvider.getBasic(path.waypoints), + })), + ); + } catch (e: any) { + throw new Error('Unable to find a route for given waypoints'); } + + let driverDistance: number | undefined; + let driverDuration: number | undefined; + let passengerDistance: number | undefined; + let passengerDuration: number | undefined; + let points: Point[] | undefined; + let fwdAzimuth: number | undefined; + let backAzimuth: number | undefined; + typedRoutes.forEach((typedRoute: TypedRoute) => { + if (typedRoute.type !== PathType.PASSENGER) { + driverDistance = typedRoute.route.distance; + driverDuration = typedRoute.route.duration; + points = typedRoute.route.points; + fwdAzimuth = typedRoute.route.fwdAzimuth; + backAzimuth = typedRoute.route.backAzimuth; + } + if (typedRoute.type !== PathType.DRIVER) { + passengerDistance = typedRoute.route.distance; + passengerDuration = typedRoute.route.duration; + if (!points) points = typedRoute.route.points; + if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; + if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; + } + }); + if (points && fwdAzimuth && backAzimuth) { + const ad = AdEntity.create({ + id: command.id, + driver: command.driver, + passenger: command.passenger, + frequency: command.frequency, + fromDate: command.fromDate, + toDate: command.toDate, + schedule: command.schedule, + seatsProposed: command.seatsProposed, + seatsRequested: command.seatsRequested, + strict: command.strict, + waypoints: command.waypoints, + points, + driverDistance, + driverDuration, + passengerDistance, + passengerDuration, + fwdAzimuth, + backAzimuth, + }); + try { + await this.repository.insertExtra(ad, 'ad'); + return ad.id; + } catch (error: any) { + if (error instanceof ConflictException) { + throw new AdAlreadyExistsException(error); + } + throw error; + } + } + throw new Error('Route error'); } } diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index 7fe7d78..93a4cda 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -1,10 +1,10 @@ +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { MatchEntity } from '../../../domain/match.entity'; -import { Candidate } from '../../types/algorithm.types'; import { MatchQuery } from './match.query'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; export abstract class Algorithm { - protected candidates: Candidate[]; + protected candidates: CandidateEntity[]; protected selector: Selector; protected processors: Processor[]; constructor( @@ -20,8 +20,9 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } - return this.candidates.map((candidate: Candidate) => - MatchEntity.create({ adId: candidate.ad.id }), + // console.log(this.candidates); + return this.candidates.map((candidate: CandidateEntity) => + MatchEntity.create({ adId: candidate.id }), ); }; } @@ -36,7 +37,7 @@ export abstract class Selector { this.query = query; this.repository = repository; } - abstract select(): Promise; + abstract select(): Promise; } /** @@ -47,5 +48,5 @@ export abstract class Processor { constructor(query: MatchQuery) { this.query = query; } - abstract execute(candidates: Candidate[]): Promise; + abstract execute(candidates: CandidateEntity[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts index 883edd0..ee155b3 100644 --- a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts +++ b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts @@ -1,9 +1,9 @@ -import { Candidate } from '../../../types/algorithm.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Processor } from '../algorithm.abstract'; export abstract class Completer extends Processor { - execute = async (candidates: Candidate[]): Promise => + execute = async (candidates: CandidateEntity[]): Promise => this.complete(candidates); - abstract complete(candidates: Candidate[]): Promise; + abstract complete(candidates: CandidateEntity[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts index 9f2f4ee..8a7104b 100644 --- a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts @@ -1,7 +1,20 @@ -import { Candidate } from '../../../types/algorithm.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Completer } from './completer.abstract'; +/** + * Complete candidates by setting driver and crew waypoints + */ export class PassengerOrientedWaypointsCompleter extends Completer { - complete = async (candidates: Candidate[]): Promise => - candidates; + complete = async ( + candidates: CandidateEntity[], + ): Promise => candidates; } + +// complete = async (candidates: Candidate[]): Promise => { +// candidates.forEach( (candidate: Candidate) => { +// if (candidate.role == Role.DRIVER) { +// candidate.driverWaypoints = th +// } + +// return candidates; +// } diff --git a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts index 8262592..f4522b9 100644 --- a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts +++ b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts @@ -1,9 +1,9 @@ -import { Candidate } from '../../../types/algorithm.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Processor } from '../algorithm.abstract'; export abstract class Filter extends Processor { - execute = async (candidates: Candidate[]): Promise => + execute = async (candidates: CandidateEntity[]): Promise => this.filter(candidates); - abstract filter(candidates: Candidate[]): Promise; + abstract filter(candidates: CandidateEntity[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts index 2d13980..ca6a558 100644 --- a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts +++ b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts @@ -1,6 +1,7 @@ -import { Candidate } from '../../../types/algorithm.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Filter } from './filter.abstract'; export class PassengerOrientedGeoFilter extends Filter { - filter = async (candidates: Candidate[]): Promise => candidates; + filter = async (candidates: CandidateEntity[]): Promise => + candidates; } diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 1b0c559..0716e19 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -1,11 +1,17 @@ import { QueryBase } from '@mobicoop/ddd-library'; import { AlgorithmType } from '../../types/algorithm.types'; import { Waypoint } from '../../types/waypoint.type'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; import { RouteProviderPort } from '../../ports/route-provider.port'; import { Route } from '@modules/geography/core/domain/route.types'; +import { + Path, + PathCreator, + PathType, + TypedRoute, +} from '@modules/ad/core/domain/patch-creator.service'; export class MatchQuery extends QueryBase { driver?: boolean; @@ -168,10 +174,14 @@ export class MatchQuery extends QueryBase { }; setRoutes = async (routeProvider: RouteProviderPort): Promise => { + const roles: Role[] = []; + if (this.driver) roles.push(Role.DRIVER); + if (this.passenger) roles.push(Role.PASSENGER); + const pathCreator: PathCreator = new PathCreator(roles, this.waypoints); try { ( await Promise.all( - this._getPaths().map(async (path: Path) => ({ + pathCreator.getPaths().map(async (path: Path) => ({ type: path.type, route: await routeProvider.getBasic(path.waypoints), })), @@ -192,43 +202,6 @@ export class MatchQuery extends QueryBase { } return this; }; - - private _getPaths = (): Path[] => { - const paths: Path[] = []; - if (this.driver && this.passenger) { - if (this.waypoints.length == 2) { - // 2 points => same route for driver and passenger - paths.push(this._createGenericPath(this.waypoints)); - } else { - paths.push( - this._createDriverPath(this.waypoints), - this._createPassengerPath(this.waypoints), - ); - } - } else if (this.driver) { - paths.push(this._createDriverPath(this.waypoints)); - } else if (this.passenger) { - paths.push(this._createPassengerPath(this.waypoints)); - } - return paths; - }; - - private _createGenericPath = (waypoints: Waypoint[]): Path => - this._createPath(waypoints, PathType.GENERIC); - - private _createDriverPath = (waypoints: Waypoint[]): Path => - this._createPath(waypoints, PathType.DRIVER); - - private _createPassengerPath = (waypoints: Waypoint[]): Path => - this._createPath( - [waypoints[0], waypoints[waypoints.length - 1]], - PathType.PASSENGER, - ); - - private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({ - type, - waypoints, - }); } export type ScheduleItem = { @@ -254,19 +227,3 @@ interface DefaultAlgorithmParameters { maxDetourDistanceRatio: number; maxDetourDurationRatio: number; } - -type Path = { - type: PathType; - waypoints: Waypoint[]; -}; - -enum PathType { - GENERIC = 'generic', - DRIVER = 'driver', - PASSENGER = 'passenger', -} - -type TypedRoute = { - type: PathType; - route: Route; -}; diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 55a394d..05ab801 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -1,13 +1,13 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; -import { Candidate } from '../../../types/algorithm.types'; import { Selector } from '../algorithm.abstract'; import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; import { ScheduleItem } from '../match.query'; import { Waypoint } from '../../../types/waypoint.type'; import { Point } from '../../../types/point.type'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; export class PassengerOrientedSelector extends Selector { - select = async (): Promise => { + select = async (): Promise => { const queryStringRoles: QueryStringRole[] = []; if (this.query.driver) queryStringRoles.push({ @@ -32,14 +32,11 @@ export class PassengerOrientedSelector extends Selector { ) ) .map((adsRole: AdsRole) => - adsRole.ads.map( - (adReadModel: AdReadModel) => - { - ad: { - id: adReadModel.uuid, - }, - role: adsRole.role, - }, + adsRole.ads.map((adReadModel: AdReadModel) => + CandidateEntity.create({ + id: adReadModel.uuid, + role: adsRole.role, + }), ), ) .flat(); diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts index d26b41d..4d1fd81 100644 --- a/src/modules/ad/core/application/types/algorithm.types.ts +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -11,7 +11,8 @@ export enum AlgorithmType { export type Candidate = { ad: Ad; role: Role; - waypoints: Waypoint[]; + driverWaypoints: Waypoint[]; + crewWaypoints: Waypoint[]; }; export type Ad = { diff --git a/src/modules/ad/core/application/types/carpool-route.type.ts b/src/modules/ad/core/application/types/carpool-route.type.ts deleted file mode 100644 index 7b23bfb..0000000 --- a/src/modules/ad/core/application/types/carpool-route.type.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Point } from './point.type'; - -/** - * A carpool route is a route with distance and duration as driver and / or passenger - */ -export type Route = { - driverDistance?: number; - driverDuration?: number; - passengerDistance?: number; - passengerDuration?: number; - fwdAzimuth: number; - backAzimuth: number; - points: Point[]; -}; diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts new file mode 100644 index 0000000..6da779e --- /dev/null +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -0,0 +1,15 @@ +import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; +import { CandidateProps, CreateCandidateProps } from './candidate.types'; + +export class CandidateEntity extends AggregateRoot { + protected readonly _id: AggregateID; + + static create = (create: CreateCandidateProps): CandidateEntity => { + const props: CandidateProps = { ...create }; + return new CandidateEntity({ id: create.id, props }); + }; + + validate(): void { + // entity business rules validation to protect it's invariant before saving entity to a database + } +} diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts new file mode 100644 index 0000000..0aaf52e --- /dev/null +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -0,0 +1,18 @@ +import { Role } from './ad.types'; + +// All properties that a Candidate has +export interface CandidateProps { + role: Role; +} + +// Properties that are needed for a Candidate creation +export interface CreateCandidateProps { + id: string; + role: Role; +} + +export type Waypoint = { + lon: number; + lat: number; + position: number; +}; diff --git a/src/modules/ad/core/domain/patch-creator.service.ts b/src/modules/ad/core/domain/patch-creator.service.ts new file mode 100644 index 0000000..4ac4668 --- /dev/null +++ b/src/modules/ad/core/domain/patch-creator.service.ts @@ -0,0 +1,77 @@ +import { Route } from '@modules/geography/core/domain/route.types'; +import { Role } from './ad.types'; +import { Waypoint } from './candidate.types'; + +export class PathCreator { + constructor( + private readonly roles: Role[], + private readonly waypoints: Waypoint[], + ) {} + + public getPaths = (): Path[] => { + const paths: Path[] = []; + if ( + this.roles.includes(Role.DRIVER) && + this.roles.includes(Role.PASSENGER) + ) { + if (this.waypoints.length == 2) { + // 2 points => same route for driver and passenger + paths.push(this._createGenericPath()); + } else { + paths.push(this._createDriverPath(), this._createPassengerPath()); + } + } else if (this.roles.includes(Role.DRIVER)) { + paths.push(this._createDriverPath()); + } else if (this.roles.includes(Role.PASSENGER)) { + paths.push(this._createPassengerPath()); + } + return paths; + }; + + private _createGenericPath = (): Path => + this._createPath(this.waypoints, PathType.GENERIC); + + private _createDriverPath = (): Path => + this._createPath(this.waypoints, PathType.DRIVER); + + private _createPassengerPath = (): Path => + this._createPath( + [this._firstWaypoint(), this._lastWaypoint()], + PathType.PASSENGER, + ); + + private _firstWaypoint = (): Waypoint => + this.waypoints.find( + (waypoint: Waypoint) => waypoint.position == 0, + ) as Waypoint; + + private _lastWaypoint = (): Waypoint => + this.waypoints.find( + (waypoint: Waypoint) => + waypoint.position == + Math.max( + ...this.waypoints.map((waypoint: Waypoint) => waypoint.position), + ), + ) as Waypoint; + + private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({ + type, + waypoints, + }); +} + +export type Path = { + type: PathType; + waypoints: Waypoint[]; +}; + +export type TypedRoute = { + type: PathType; + route: Route; +}; + +export enum PathType { + GENERIC = 'generic', + DRIVER = 'driver', + PASSENGER = 'passenger', +} diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index d0cfb3e..6db752e 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -60,27 +60,40 @@ const mockAdRepository = { }; const mockRouteProvider: RouteProviderPort = { - getBasic: jest.fn().mockImplementation(() => ({ - distance: 350101, - duration: 14422, - fwdAzimuth: 273, - backAzimuth: 93, - distanceAzimuth: 336544, - points: [ - { - lon: 6.1765102, - lat: 48.689445, - }, - { - lon: 4.984578, - lat: 48.725687, - }, - { - lon: 2.3522, - lat: 48.8566, - }, - ], - })), + getBasic: jest + .fn() + .mockImplementationOnce(() => { + throw new Error(); + }) + .mockImplementationOnce(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: undefined, + })) + .mockImplementation(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + })), }; describe('create-ad.service', () => { @@ -110,6 +123,16 @@ describe('create-ad.service', () => { describe('execution', () => { const createAdCommand = new CreateAdCommand(createAdProps); + it('should throw an error if route cant be computed', async () => { + await expect( + createAdService.execute(createAdCommand), + ).rejects.toBeInstanceOf(Error); + }); + it('should throw an error if route is corrupted', async () => { + await expect( + createAdService.execute(createAdCommand), + ).rejects.toBeInstanceOf(Error); + }); it('should create a new ad', async () => { AdEntity.create = jest.fn().mockReturnValue({ id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', @@ -120,17 +143,11 @@ describe('create-ad.service', () => { expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da'); }); it('should throw an error if something bad happens', async () => { - AdEntity.create = jest.fn().mockReturnValue({ - id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', - }); await expect( createAdService.execute(createAdCommand), ).rejects.toBeInstanceOf(Error); }); it('should throw an exception if Ad already exists', async () => { - AdEntity.create = jest.fn().mockReturnValue({ - id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', - }); await expect( createAdService.execute(createAdCommand), ).rejects.toBeInstanceOf(AdAlreadyExistsException); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 43f9533..f75ef21 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -1,11 +1,9 @@ import { PassengerOrientedGeoFilter } from '@modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; -import { - AlgorithmType, - Candidate, -} from '@modules/ad/core/application/types/algorithm.types'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; const originWaypoint: Waypoint = { position: 0, @@ -42,50 +40,22 @@ const matchQuery = new MatchQuery({ waypoints: [originWaypoint, destinationWaypoint], }); -const candidates: Candidate[] = [ - { - ad: { - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - }, +const candidates: CandidateEntity[] = [ + CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, - waypoints: [ - { - position: 0, - lat: 48.68874, - lon: 6.18546, - }, - { - position: 1, - lat: 48.87845, - lon: 2.36547, - }, - ], - }, - { - ad: { - id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', - }, + }), + CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, - waypoints: [ - { - position: 0, - lat: 48.69844, - lon: 6.168484, - }, - { - position: 1, - lat: 48.855648, - lon: 2.34645, - }, - ], - }, + }), ]; describe('Passenger oriented geo filter', () => { it('should filter candidates', async () => { const passengerOrientedGeoFilter: PassengerOrientedGeoFilter = new PassengerOrientedGeoFilter(matchQuery); - const filteredCandidates: Candidate[] = + const filteredCandidates: CandidateEntity[] = await passengerOrientedGeoFilter.filter(candidates); expect(filteredCandidates.length).toBe(2); }); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index 85911f1..954a021 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -1,12 +1,10 @@ import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector'; -import { - AlgorithmType, - Candidate, -} from '@modules/ad/core/application/types/algorithm.types'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; const originWaypoint: Waypoint = { position: 0, @@ -136,7 +134,8 @@ describe('Passenger oriented selector', () => { it('should select candidates', async () => { const passengerOrientedSelector: PassengerOrientedSelector = new PassengerOrientedSelector(matchQuery, mockMatcherRepository); - const candidates: Candidate[] = await passengerOrientedSelector.select(); + const candidates: CandidateEntity[] = + await passengerOrientedSelector.select(); expect(candidates.length).toBe(2); }); }); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts index 43d898b..e06e82b 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts @@ -1,11 +1,9 @@ import { PassengerOrientedWaypointsCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; -import { - AlgorithmType, - Candidate, -} from '@modules/ad/core/application/types/algorithm.types'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; const originWaypoint: Waypoint = { position: 0, @@ -42,50 +40,22 @@ const matchQuery = new MatchQuery({ waypoints: [originWaypoint, destinationWaypoint], }); -const candidates: Candidate[] = [ - { - ad: { - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - }, +const candidates: CandidateEntity[] = [ + CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, - waypoints: [ - { - position: 0, - lat: 48.69, - lon: 6.18, - }, - { - position: 1, - lat: 48.87, - lon: 2.37, - }, - ], - }, - { - ad: { - id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', - }, + }), + CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, - waypoints: [ - { - position: 0, - lat: 48.63584, - lon: 6.148754, - }, - { - position: 1, - lat: 48.89874, - lon: 2.368745, - }, - ], - }, + }), ]; describe('Passenger oriented waypoints completer', () => { it('should complete candidates', async () => { const passengerOrientedWaypointsCompleter: PassengerOrientedWaypointsCompleter = new PassengerOrientedWaypointsCompleter(matchQuery); - const completedCandidates: Candidate[] = + const completedCandidates: CandidateEntity[] = await passengerOrientedWaypointsCompleter.complete(candidates); expect(completedCandidates.length).toBe(2); }); diff --git a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts new file mode 100644 index 0000000..6a9c693 --- /dev/null +++ b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts @@ -0,0 +1,78 @@ +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Waypoint } from '@modules/ad/core/domain/candidate.types'; +import { + Path, + PathCreator, + PathType, +} from '@modules/ad/core/domain/patch-creator.service'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, +}; +const intermediateWaypoint: Waypoint = { + position: 1, + lat: 48.74488, + lon: 4.8972, +}; + +describe('Path Creator Service', () => { + it('should create a path for a driver only', () => { + const pathCreator: PathCreator = new PathCreator( + [Role.DRIVER], + [originWaypoint, destinationWaypoint], + ); + const paths: Path[] = pathCreator.getPaths(); + expect(paths).toHaveLength(1); + expect(paths[0].type).toBe(PathType.DRIVER); + }); + it('should create a path for a passenger only', () => { + const pathCreator: PathCreator = new PathCreator( + [Role.PASSENGER], + [originWaypoint, destinationWaypoint], + ); + const paths: Path[] = pathCreator.getPaths(); + expect(paths).toHaveLength(1); + expect(paths[0].type).toBe(PathType.PASSENGER); + }); + it('should create a single path for a driver and passenger', () => { + const pathCreator: PathCreator = new PathCreator( + [Role.DRIVER, Role.PASSENGER], + [originWaypoint, destinationWaypoint], + ); + const paths: Path[] = pathCreator.getPaths(); + expect(paths).toHaveLength(1); + expect(paths[0].type).toBe(PathType.GENERIC); + }); + it('should create two different paths for a driver and passenger with intermediate waypoint', () => { + const pathCreator: PathCreator = new PathCreator( + [Role.DRIVER, Role.PASSENGER], + [ + originWaypoint, + intermediateWaypoint, + { ...destinationWaypoint, position: 2 }, + ], + ); + const paths: Path[] = pathCreator.getPaths(); + expect(paths).toHaveLength(2); + expect( + paths.filter((path: Path) => path.type == PathType.DRIVER), + ).toHaveLength(1); + expect( + paths.filter((path: Path) => path.type == PathType.DRIVER)[0].waypoints, + ).toHaveLength(3); + expect( + paths.filter((path: Path) => path.type == PathType.PASSENGER), + ).toHaveLength(1); + expect( + paths.filter((path: Path) => path.type == PathType.PASSENGER)[0] + .waypoints, + ).toHaveLength(2); + }); +}); From 1939f62049a1e451600f537a66b931a098f7dd23 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 12 Sep 2023 14:40:16 +0200 Subject: [PATCH 22/52] fixed bad ad mapping --- src/modules/ad/ad.mapper.ts | 60 ++++++------ .../application/ports/ad.repository.port.ts | 3 +- .../selector/passenger-oriented.selector.ts | 14 +-- src/modules/ad/core/domain/candidate.types.ts | 2 + .../ad/infrastructure/ad.repository.ts | 42 ++++++--- .../unit/core/match.query-handler.spec.ts | 10 +- .../core/passenger-oriented-algorithm.spec.ts | 12 +-- .../passenger-oriented-geo-filter.spec.ts | 24 +++++ .../core/passenger-oriented-selector.spec.ts | 54 +++++------ ...enger-oriented-waypoints-completer.spec.ts | 24 +++++ .../unit/infrastructure/ad.repository.spec.ts | 92 ++++++++++++++++--- .../geography/core/domain/route.entity.ts | 43 --------- .../geography/core/domain/route.types.ts | 2 - 13 files changed, 238 insertions(+), 144 deletions(-) diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 731bf73..6fbc9b7 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -7,10 +7,14 @@ import { AdWriteExtraModel, } from './infrastructure/ad.repository'; import { v4 } from 'uuid'; -import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object'; +import { + ScheduleItem, + ScheduleItemProps, +} from './core/domain/value-objects/schedule-item.value-object'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; import { AD_DIRECTION_ENCODER } from './ad.di-tokens'; import { ExtendedMapper } from '@mobicoop/ddd-library'; +import { Waypoint } from './core/domain/value-objects/waypoint.value-object'; /** * Mapper constructs objects that are used in different layers: @@ -76,28 +80,12 @@ export class AdMapper return record; }; - toDomain = (record: AdReadModel): AdEntity => { - const entity = new AdEntity({ + toDomain = (record: AdReadModel): AdEntity => + new AdEntity({ id: record.uuid, createdAt: new Date(record.createdAt), updatedAt: new Date(record.updatedAt), props: { - driver: record.driver, - passenger: record.passenger, - frequency: record.frequency, - fromDate: record.fromDate.toISOString().split('T')[0], - toDate: record.toDate.toISOString().split('T')[0], - schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({ - day: scheduleItem.day, - time: `${scheduleItem.time - .getUTCHours() - .toString() - .padStart(2, '0')}:${scheduleItem.time - .getUTCMinutes() - .toString() - .padStart(2, '0')}`, - margin: scheduleItem.margin, - })), seatsProposed: record.seatsProposed, seatsRequested: record.seatsRequested, strict: record.strict, @@ -105,19 +93,37 @@ export class AdMapper driverDistance: record.driverDistance, passengerDuration: record.passengerDuration, passengerDistance: record.passengerDistance, - waypoints: this.directionEncoder - .decode(record.waypoints) - .map((coordinates, index) => ({ - position: index, - ...coordinates, - })), + driver: record.driver, + passenger: record.passenger, + frequency: record.frequency, + fromDate: record.fromDate.toISOString().split('T')[0], + toDate: record.toDate.toISOString().split('T')[0], + schedule: record.schedule.map( + (scheduleItem: ScheduleItemModel) => + new ScheduleItem({ + day: scheduleItem.day, + time: `${scheduleItem.time + .getUTCHours() + .toString() + .padStart(2, '0')}:${scheduleItem.time + .getUTCMinutes() + .toString() + .padStart(2, '0')}`, + margin: scheduleItem.margin, + }), + ), + waypoints: this.directionEncoder.decode(record.waypoints).map( + (coordinates, index) => + new Waypoint({ + position: index, + ...coordinates, + }), + ), fwdAzimuth: record.fwdAzimuth, backAzimuth: record.backAzimuth, points: [], }, }); - return entity; - }; toPersistenceExtra = (entity: AdEntity): AdWriteExtraModel => ({ waypoints: this.directionEncoder.encode(entity.getProps().waypoints), diff --git a/src/modules/ad/core/application/ports/ad.repository.port.ts b/src/modules/ad/core/application/ports/ad.repository.port.ts index a123015..20eb6a3 100644 --- a/src/modules/ad/core/application/ports/ad.repository.port.ts +++ b/src/modules/ad/core/application/ports/ad.repository.port.ts @@ -1,7 +1,6 @@ import { ExtendedRepositoryPort } from '@mobicoop/ddd-library'; import { AdEntity } from '../../domain/ad.entity'; -import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; export type AdRepositoryPort = ExtendedRepositoryPort & { - getCandidates(queryString: string): Promise; + getCandidateAds(queryString: string): Promise; }; diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 05ab801..dea1679 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -1,10 +1,10 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Selector } from '../algorithm.abstract'; -import { AdReadModel } from '@modules/ad/infrastructure/ad.repository'; import { ScheduleItem } from '../match.query'; import { Waypoint } from '../../../types/waypoint.type'; import { Point } from '../../../types/point.type'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; export class PassengerOrientedSelector extends Selector { select = async (): Promise => { @@ -25,17 +25,18 @@ export class PassengerOrientedSelector extends Selector { queryStringRoles.map>( async (queryStringRole: QueryStringRole) => { - ads: await this.repository.getCandidates(queryStringRole.query), + ads: await this.repository.getCandidateAds(queryStringRole.query), role: queryStringRole.role, }, ), ) ) .map((adsRole: AdsRole) => - adsRole.ads.map((adReadModel: AdReadModel) => + adsRole.ads.map((adEntity: AdEntity) => CandidateEntity.create({ - id: adReadModel.uuid, + id: adEntity.id, role: adsRole.role, + waypoints: adEntity.getProps().waypoints, }), ), ) @@ -60,7 +61,8 @@ export class PassengerOrientedSelector extends Selector { "seatsProposed","seatsRequested",\ strict,\ "fwdAzimuth","backAzimuth",\ - si.day,si.time,si.margin`, + ad."createdAt",ad."updatedAt",\ + si.uuid as "scheduleItemUuid",si.day,si.time,si.margin,si."createdAt" as "scheduleItemCreatedAt",si."updatedAt" as "scheduleItemUpdatedAt"`, role == Role.DRIVER ? this._selectAsDriver() : this._selectAsPassenger(), ].join(); @@ -295,6 +297,6 @@ export type QueryStringRole = { }; type AdsRole = { - ads: AdReadModel[]; + ads: AdEntity[]; role: Role; }; diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 0aaf52e..65e8cd6 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -3,12 +3,14 @@ import { Role } from './ad.types'; // All properties that a Candidate has export interface CandidateProps { role: Role; + waypoints: Waypoint[]; } // Properties that are needed for a Candidate creation export interface CreateCandidateProps { id: string; role: Role; + waypoints: Waypoint[]; } export type Waypoint = { diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index c7fd343..d5b0f24 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -29,11 +29,17 @@ export type AdModel = { updatedAt: Date; }; +/** + * The record as returned by the peristence system + */ export type AdReadModel = AdModel & { waypoints: string; schedule: ScheduleItemModel[]; }; +/** + * The record ready to be sent to the peristence system + */ export type AdWriteModel = AdModel & { schedule: { create: ScheduleItemModel[]; @@ -59,11 +65,14 @@ export type ScheduleItemModel = ScheduleItem & { export type UngroupedAdModel = AdModel & ScheduleItem & { + scheduleItemUuid: string; + scheduleItemCreatedAt: Date; + scheduleItemUpdatedAt: Date; waypoints: string; }; export type GroupedAdModel = AdModel & { - schedule: ScheduleItem[]; + schedule: ScheduleItemModel[]; waypoints: string; }; @@ -100,14 +109,18 @@ export class AdRepository ); } - getCandidates = async (queryString: string): Promise => { - // console.log(queryString); - return this.toAdReadModels( + getCandidateAds = async (queryString: string): Promise => + this._toAdReadModels( (await this.prismaRaw.$queryRawUnsafe(queryString)) as UngroupedAdModel[], - ); - }; + ) + .map((adReadModel: AdReadModel) => { + if (this.mapper.toDomain) return this.mapper.toDomain(adReadModel); + }) + .filter( + (adEntity: AdEntity | undefined) => adEntity !== undefined, + ) as AdEntity[]; - private toAdReadModels = ( + private _toAdReadModels = ( ungroupedAds: UngroupedAdModel[], ): AdReadModel[] => { const groupedAdModels: GroupedAdModel[] = ungroupedAds.map( @@ -120,9 +133,12 @@ export class AdRepository toDate: ungroupedAd.toDate, schedule: [ { + uuid: ungroupedAd.scheduleItemUuid, day: ungroupedAd.day, time: ungroupedAd.time, margin: ungroupedAd.margin, + createdAt: ungroupedAd.scheduleItemCreatedAt, + updatedAt: ungroupedAd.scheduleItemUpdatedAt, }, ], seatsProposed: ungroupedAd.seatsProposed, @@ -140,14 +156,14 @@ export class AdRepository }), ); const adReadModels: AdReadModel[] = []; - groupedAdModels.forEach((adReadModel: AdReadModel) => { - const ad: AdReadModel | undefined = adReadModels.find( - (arm: AdReadModel) => arm.uuid == adReadModel.uuid, + groupedAdModels.forEach((groupdeAdModel: GroupedAdModel) => { + const adReadModel: AdReadModel | undefined = adReadModels.find( + (arm: AdReadModel) => arm.uuid == groupdeAdModel.uuid, ); - if (ad) { - ad.schedule.push(...adReadModel.schedule); + if (adReadModel) { + adReadModel.schedule.push(...groupdeAdModel.schedule); } else { - adReadModels.push(adReadModel); + adReadModels.push(groupdeAdModel); } }); return adReadModels; diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 275b97f..5f99b43 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -35,12 +35,12 @@ const destinationWaypoint: Waypoint = { }; const mockAdRepository = { - getCandidates: jest.fn().mockImplementation(() => [ + getCandidateAds: jest.fn().mockImplementation(() => [ { - ad: { - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - }, - role: Role.DRIVER, + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + getProps: jest.fn().mockImplementation(() => ({ + role: Role.DRIVER, + })), }, ]), }; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index 5a17a2c..66a1bb4 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -3,7 +3,7 @@ import { MatchQuery } from '@modules/ad/core/application/queries/match/match.que import { PassengerOrientedAlgorithm } from '@modules/ad/core/application/queries/match/passenger-oriented-algorithm'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; -import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; const originWaypoint: Waypoint = { @@ -51,12 +51,12 @@ const mockMatcherRepository: AdRepositoryPort = { delete: jest.fn(), count: jest.fn(), healthCheck: jest.fn(), - getCandidates: jest.fn().mockImplementation(() => [ + getCandidateAds: jest.fn().mockImplementation(() => [ { - ad: { - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - }, - role: Role.DRIVER, + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + getProps: jest.fn().mockImplementation(() => ({ + waypoints: [], + })), }, ]), }; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index f75ef21..27b497b 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -44,10 +44,34 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + waypoints: [ + { + position: 0, + lat: 48.678454, + lon: 6.189745, + }, + { + position: 1, + lat: 48.84877, + lon: 2.398457, + }, + ], }), CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + waypoints: [ + { + position: 0, + lat: 48.668487, + lon: 6.178457, + }, + { + position: 1, + lat: 48.897457, + lon: 2.3688487, + }, + ], }), ]; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index 954a021..4659916 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -99,33 +99,35 @@ const mockMatcherRepository: AdRepositoryPort = { delete: jest.fn(), count: jest.fn(), healthCheck: jest.fn(), - getCandidates: jest.fn().mockImplementation(() => [ + getCandidateAds: jest.fn().mockImplementation(() => [ { - uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', - driver: true, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: new Date('2023-06-21'), - toDate: new Date('2023-06-21'), - seatsProposed: 3, - seatsRequested: 1, - strict: false, - ddriverDistance: 350000, - driverDuration: 14400, - passengerDistance: 350000, - passengerDuration: 14400, - fwdAzimuth: 273, - backAzimuth: 93, - createdAt: new Date('2023-06-20T17:05:00Z'), - updatedAt: new Date('2023-06-20T17:05:00Z'), - waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', - schedule: [ - { - day: 3, - time: new Date('2023-06-21T07:05:00Z'), - margin: 900, - }, - ], + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + getProps: jest.fn().mockImplementation(() => ({ + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: new Date('2023-06-21'), + toDate: new Date('2023-06-21'), + seatsProposed: 3, + seatsRequested: 1, + strict: false, + ddriverDistance: 350000, + driverDuration: 14400, + passengerDistance: 350000, + passengerDuration: 14400, + fwdAzimuth: 273, + backAzimuth: 93, + createdAt: new Date('2023-06-20T17:05:00Z'), + updatedAt: new Date('2023-06-20T17:05:00Z'), + waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + schedule: [ + { + day: 3, + time: new Date('2023-06-21T07:05:00Z'), + margin: 900, + }, + ], + })), }, ]), }; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts index e06e82b..5900ae5 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts @@ -44,10 +44,34 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + waypoints: [ + { + position: 0, + lat: 48.678454, + lon: 6.189745, + }, + { + position: 1, + lat: 48.84877, + lon: 2.398457, + }, + ], }), CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + waypoints: [ + { + position: 0, + lat: 48.668487, + lon: 6.178457, + }, + { + position: 1, + lat: 48.897457, + lon: 2.3688487, + }, + ], }), ]; diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index 1ba6e92..064349c 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -5,11 +5,9 @@ import { } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { Frequency } from '@modules/ad/core/domain/ad.types'; -import { - AdReadModel, - AdRepository, -} from '@modules/ad/infrastructure/ad.repository'; +import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; import { EventEmitterModule } from '@nestjs/event-emitter'; @@ -21,7 +19,58 @@ const mockMessagePublisher = { const mockDirectionEncoder: DirectionEncoderPort = { encode: jest.fn(), - decode: jest.fn(), + decode: jest + .fn() + .mockImplementationOnce(() => [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ]) + .mockImplementationOnce(() => [ + { + lon: 6.1765109, + lat: 48.689455, + }, + { + lon: 2.3598, + lat: 48.8589, + }, + ]) + .mockImplementationOnce(() => [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ]) + .mockImplementationOnce(() => [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ]) + .mockImplementationOnce(() => [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ]), }; const mockRouteProvider: RouteProviderPort = { @@ -43,7 +92,7 @@ const mockPrismaService = { seatsProposed: 3, seatsRequested: 1, strict: false, - ddriverDistance: 350000, + driverDistance: 350000, driverDuration: 14400, passengerDistance: 350000, passengerDuration: 14400, @@ -52,9 +101,12 @@ const mockPrismaService = { createdAt: new Date('2023-06-20T17:05:00Z'), updatedAt: new Date('2023-06-20T17:05:00Z'), waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + scheduleItemUuid: 'b6bfac1f-e62e-4622-9641-a3475e15fc00', day: 3, time: new Date('2023-06-21T07:05:00Z'), margin: 900, + scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'), + scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'), }, { uuid: '84af18ff-8779-4cac-9651-1ed5ab0713c4', @@ -66,7 +118,7 @@ const mockPrismaService = { seatsProposed: 3, seatsRequested: 1, strict: false, - ddriverDistance: 349000, + driverDistance: 349000, driverDuration: 14300, passengerDistance: 350000, passengerDuration: 14400, @@ -75,9 +127,12 @@ const mockPrismaService = { createdAt: new Date('2023-06-18T14:16:10Z'), updatedAt: new Date('2023-06-18T14:16:10Z'), waypoints: 'LINESTRING(6.1765109 48.689455,2.3598 48.8589)', + scheduleItemUuid: '01524541-2044-49dc-8be6-1a3ccdc653b0', day: 3, time: new Date('2023-06-21T07:14:00Z'), margin: 900, + scheduleItemCreatedAt: new Date('2023-06-18T14:16:10Z'), + scheduleItemUpdatedAt: new Date('2023-06-18T14:16:10Z'), }, ]; }) @@ -93,7 +148,7 @@ const mockPrismaService = { seatsProposed: 3, seatsRequested: 1, strict: false, - ddriverDistance: 350000, + driverDistance: 350000, driverDuration: 14400, passengerDistance: 350000, passengerDuration: 14400, @@ -102,9 +157,12 @@ const mockPrismaService = { createdAt: new Date('2023-06-20T17:05:00Z'), updatedAt: new Date('2023-06-20T17:05:00Z'), waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + scheduleItemUuid: '1387b34f-8ab1-46e0-8d0f-803af0f40f28', day: 3, time: new Date('2023-06-21T07:05:00Z'), margin: 900, + scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'), + scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'), }, { uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', @@ -116,7 +174,7 @@ const mockPrismaService = { seatsProposed: 3, seatsRequested: 1, strict: false, - ddriverDistance: 350000, + driverDistance: 350000, driverDuration: 14400, passengerDistance: 350000, passengerDuration: 14400, @@ -125,9 +183,12 @@ const mockPrismaService = { createdAt: new Date('2023-06-20T17:05:00Z'), updatedAt: new Date('2023-06-20T17:05:00Z'), waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + scheduleItemUuid: '1fa88104-c50b-4f10-b8ce-389df765f3a6', day: 4, time: new Date('2023-06-21T07:15:00Z'), margin: 900, + scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'), + scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'), }, { uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777', @@ -139,7 +200,7 @@ const mockPrismaService = { seatsProposed: 3, seatsRequested: 1, strict: false, - ddriverDistance: 350000, + driverDistance: 350000, driverDuration: 14400, passengerDistance: 350000, passengerDuration: 14400, @@ -148,9 +209,12 @@ const mockPrismaService = { createdAt: new Date('2023-06-20T17:05:00Z'), updatedAt: new Date('2023-06-20T17:05:00Z'), waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)', + scheduleItemUuid: '760bb1bb-256b-4e79-9d82-6d13011118f1', day: 5, time: new Date('2023-06-21T07:16:00Z'), margin: 900, + scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'), + scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'), }, ]; }) @@ -194,22 +258,22 @@ describe('Ad repository', () => { }); it('should get candidates if query returns punctual Ads', async () => { - const candidates: AdReadModel[] = await adRepository.getCandidates( + const candidates: AdEntity[] = await adRepository.getCandidateAds( 'somePunctualQueryString', ); expect(candidates.length).toBe(2); }); it('should get candidates if query returns recurrent Ads', async () => { - const candidates: AdReadModel[] = await adRepository.getCandidates( + const candidates: AdEntity[] = await adRepository.getCandidateAds( 'someRecurrentQueryString', ); expect(candidates.length).toBe(1); - expect(candidates[0].schedule.length).toBe(3); + expect(candidates[0].getProps().schedule.length).toBe(3); }); it('should return an empty array of candidates if query does not return Ads', async () => { - const candidates: AdReadModel[] = await adRepository.getCandidates( + const candidates: AdEntity[] = await adRepository.getCandidateAds( 'someQueryString', ); expect(candidates.length).toBe(0); diff --git a/src/modules/geography/core/domain/route.entity.ts b/src/modules/geography/core/domain/route.entity.ts index 373d3aa..7a18c96 100644 --- a/src/modules/geography/core/domain/route.entity.ts +++ b/src/modules/geography/core/domain/route.entity.ts @@ -29,47 +29,4 @@ export class RouteEntity extends AggregateRoot { validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } - - // private static getPaths = ( - // roles: Role[], - // waypoints: WaypointProps[], - // ): Path[] => { - // const paths: Path[] = []; - // if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { - // if (waypoints.length == 2) { - // // 2 points => same route for driver and passenger - // paths.push(this.createGenericPath(waypoints)); - // } else { - // paths.push( - // this.createDriverPath(waypoints), - // this.createPassengerPath(waypoints), - // ); - // } - // } else if (roles.includes(Role.DRIVER)) { - // paths.push(this.createDriverPath(waypoints)); - // } else if (roles.includes(Role.PASSENGER)) { - // paths.push(this.createPassengerPath(waypoints)); - // } - // return paths; - // }; - - // private static createGenericPath = (waypoints: WaypointProps[]): Path => - // this.createPath(waypoints, PathType.GENERIC); - - // private static createDriverPath = (waypoints: WaypointProps[]): Path => - // this.createPath(waypoints, PathType.DRIVER); - - // private static createPassengerPath = (waypoints: WaypointProps[]): Path => - // this.createPath( - // [waypoints[0], waypoints[waypoints.length - 1]], - // PathType.PASSENGER, - // ); - - // private static createPath = ( - // points: WaypointProps[], - // type: PathType, - // ): Path => ({ - // type, - // points, - // }); } diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index 43855bc..e6df76c 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -45,5 +45,3 @@ export type Spacetime = { }; export type Step = Point & Spacetime; - -export type Waystep = Waypoint & Spacetime; From 4731020e8ad2448442bb9ab0eb898f38ef3c91c9 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 12 Sep 2023 15:10:32 +0200 Subject: [PATCH 23/52] fix wrong id for ad creation --- .../ad/core/application/queries/match/algorithm.abstract.ts | 1 - .../ad/interface/message-handlers/ad-created.message-handler.ts | 2 +- src/modules/ad/interface/message-handlers/ad.types.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index 93a4cda..f993730 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -20,7 +20,6 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } - // console.log(this.candidates); return this.candidates.map((candidate: CandidateEntity) => MatchEntity.create({ adId: candidate.id }), ); diff --git a/src/modules/ad/interface/message-handlers/ad-created.message-handler.ts b/src/modules/ad/interface/message-handlers/ad-created.message-handler.ts index 621c6a1..9e7aa88 100644 --- a/src/modules/ad/interface/message-handlers/ad-created.message-handler.ts +++ b/src/modules/ad/interface/message-handlers/ad-created.message-handler.ts @@ -16,7 +16,7 @@ export class AdCreatedMessageHandler { const createdAd: Ad = JSON.parse(message); await this.commandBus.execute( new CreateAdCommand({ - id: createdAd.id, + id: createdAd.aggregateId, driver: createdAd.driver, passenger: createdAd.passenger, frequency: createdAd.frequency, diff --git a/src/modules/ad/interface/message-handlers/ad.types.ts b/src/modules/ad/interface/message-handlers/ad.types.ts index 5bc7e88..45eb53b 100644 --- a/src/modules/ad/interface/message-handlers/ad.types.ts +++ b/src/modules/ad/interface/message-handlers/ad.types.ts @@ -1,7 +1,7 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; export type Ad = { - id: string; + aggregateId: string; driver: boolean; passenger: boolean; frequency: Frequency; From 74fb2c120e538fbfeeb39dd245183f16dcfd593d Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 13 Sep 2023 15:28:07 +0200 Subject: [PATCH 24/52] bette use of value objects --- src/modules/ad/ad.mapper.ts | 14 ++--- .../commands/create-ad/create-ad.service.ts | 18 +++++- .../application/ports/route-provider.port.ts | 2 +- .../passenger-oriented-waypoints.completer.ts | 55 +++++++++++++++- .../application/queries/match/match.query.ts | 37 +++++++---- .../ad/core/domain/candidate.entity.ts | 5 ++ src/modules/ad/core/domain/candidate.types.ts | 21 +++++-- ...tor.service.ts => path-creator.service.ts} | 4 +- .../value-objects/actor.value-object.ts | 27 ++++++++ .../value-objects/waypoint.value-object.ts | 15 +---- .../value-objects/waystep.value-object.ts | 46 ++++++++++++++ .../core/domain/waysteps-creator.service.ts | 62 +++++++++++++++++++ .../ad/infrastructure/route-provider.ts | 3 +- .../unit/core/actor.value-object.spec.ts | 14 +++++ .../unit/core/match.query-handler.spec.ts | 10 +++ ...enger-oriented-waypoints-completer.spec.ts | 8 +-- .../unit/core/path-creator.service.spec.ts | 31 ++++++---- .../unit/core/waystep.value-object.spec.ts | 62 +++++++++++++++++++ .../queries/get-route/get-route.query.ts | 2 +- .../geography/core/domain/route.types.ts | 1 + .../domain/value-objects/step.value-object.ts | 31 ++++------ .../value-objects/waypoint.value-object.ts | 15 +---- 22 files changed, 387 insertions(+), 96 deletions(-) rename src/modules/ad/core/domain/{patch-creator.service.ts => path-creator.service.ts} (94%) create mode 100644 src/modules/ad/core/domain/value-objects/actor.value-object.ts create mode 100644 src/modules/ad/core/domain/value-objects/waystep.value-object.ts create mode 100644 src/modules/ad/core/domain/waysteps-creator.service.ts create mode 100644 src/modules/ad/tests/unit/core/actor.value-object.spec.ts create mode 100644 src/modules/ad/tests/unit/core/waystep.value-object.spec.ts diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 6fbc9b7..f31f7e6 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -14,7 +14,6 @@ import { import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; import { AD_DIRECTION_ENCODER } from './ad.di-tokens'; import { ExtendedMapper } from '@mobicoop/ddd-library'; -import { Waypoint } from './core/domain/value-objects/waypoint.value-object'; /** * Mapper constructs objects that are used in different layers: @@ -112,13 +111,12 @@ export class AdMapper margin: scheduleItem.margin, }), ), - waypoints: this.directionEncoder.decode(record.waypoints).map( - (coordinates, index) => - new Waypoint({ - position: index, - ...coordinates, - }), - ), + waypoints: this.directionEncoder + .decode(record.waypoints) + .map((coordinates, index) => ({ + position: index, + ...coordinates, + })), fwdAzimuth: record.fwdAzimuth, backAzimuth: record.backAzimuth, points: [], diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 55c2788..380de24 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -13,8 +13,10 @@ import { PathCreator, PathType, TypedRoute, -} from '@modules/ad/core/domain/patch-creator.service'; +} from '@modules/ad/core/domain/path-creator.service'; import { Point } from '../../types/point.type'; +import { Waypoint } from '../../types/waypoint.type'; +import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { @@ -29,11 +31,21 @@ export class CreateAdService implements ICommandHandler { const roles: Role[] = []; if (command.driver) roles.push(Role.DRIVER); if (command.passenger) roles.push(Role.PASSENGER); - const pathCreator: PathCreator = new PathCreator(roles, command.waypoints); + const pathCreator: PathCreator = new PathCreator( + roles, + command.waypoints.map( + (waypoint: Waypoint) => + new WaypointValueObject({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + }), + ), + ); let typedRoutes: TypedRoute[]; try { typedRoutes = await Promise.all( - pathCreator.getPaths().map(async (path: Path) => ({ + pathCreator.getBasePaths().map(async (path: Path) => ({ type: path.type, route: await this.routeProvider.getBasic(path.waypoints), })), diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts index 7974886..7d86496 100644 --- a/src/modules/ad/core/application/ports/route-provider.port.ts +++ b/src/modules/ad/core/application/ports/route-provider.port.ts @@ -1,5 +1,5 @@ import { Route } from '@modules/geography/core/domain/route.types'; -import { Waypoint } from '../types/waypoint.type'; +import { Waypoint } from '../../domain/value-objects/waypoint.value-object'; export interface RouteProviderPort { /** diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts index 8a7104b..c281177 100644 --- a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts @@ -1,5 +1,12 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Completer } from './completer.abstract'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { + Waypoint as WaypointValueObject, + WaypointProps, +} from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { Waypoint } from '../../../types/waypoint.type'; +import { WayStepsCreator } from '@modules/ad/core/domain/waysteps-creator.service'; /** * Complete candidates by setting driver and crew waypoints @@ -7,7 +14,53 @@ import { Completer } from './completer.abstract'; export class PassengerOrientedWaypointsCompleter extends Completer { complete = async ( candidates: CandidateEntity[], - ): Promise => candidates; + ): Promise => { + candidates.forEach((candidate: CandidateEntity) => { + const carpoolPathCreator = new WayStepsCreator( + candidate.getProps().role == Role.DRIVER + ? candidate.getProps().waypoints.map( + (waypoint: WaypointProps) => + new WaypointValueObject({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + }), + ) + : this.query.waypoints.map( + (waypoint: Waypoint) => + new WaypointValueObject({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + }), + ), + candidate.getProps().role == Role.PASSENGER + ? candidate.getProps().waypoints.map( + (waypoint: WaypointProps) => + new WaypointValueObject({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + }), + ) + : this.query.waypoints.map( + (waypoint: Waypoint) => + new WaypointValueObject({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + }), + ), + ); + candidate.setWaySteps(carpoolPathCreator.getCrewCarpoolPath()); + }); + // console.log( + // candidates[0] + // .getProps() + // .waySteps?.map((waystep: WayStep) => waystep.actors), + // ); + return candidates; + }; } // complete = async (candidates: Candidate[]): Promise => { diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 0716e19..55a31b6 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -11,7 +11,8 @@ import { PathCreator, PathType, TypedRoute, -} from '@modules/ad/core/domain/patch-creator.service'; +} from '@modules/ad/core/domain/path-creator.service'; +import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; export class MatchQuery extends QueryBase { driver?: boolean; @@ -37,6 +38,7 @@ export class MatchQuery extends QueryBase { driverRoute?: Route; passengerRoute?: Route; backAzimuth?: number; + private readonly originWaypoint: Waypoint; constructor(props: MatchRequestDto) { super(); @@ -60,6 +62,9 @@ export class MatchQuery extends QueryBase { this.maxDetourDurationRatio = props.maxDetourDurationRatio; this.page = props.page ?? 1; this.perPage = props.perPage ?? 10; + this.originWaypoint = this.waypoints.filter( + (waypoint: Waypoint) => waypoint.position == 0, + )[0]; } setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => { @@ -126,8 +131,8 @@ export class MatchQuery extends QueryBase { date: initialFromDate, time: this.schedule[0].time, coordinates: { - lon: this.waypoints[0].lon, - lat: this.waypoints[0].lat, + lon: this.originWaypoint.lon, + lat: this.originWaypoint.lat, }, }, this.frequency, @@ -138,8 +143,8 @@ export class MatchQuery extends QueryBase { date: initialFromDate, time: this.schedule[0].time, coordinates: { - lon: this.waypoints[0].lon, - lat: this.waypoints[0].lat, + lon: this.originWaypoint.lon, + lat: this.originWaypoint.lat, }, }, this.frequency, @@ -151,8 +156,8 @@ export class MatchQuery extends QueryBase { date: this.fromDate, time: scheduleItem.time, coordinates: { - lon: this.waypoints[0].lon, - lat: this.waypoints[0].lat, + lon: this.originWaypoint.lon, + lat: this.originWaypoint.lat, }, }, this.frequency, @@ -162,8 +167,8 @@ export class MatchQuery extends QueryBase { date: this.fromDate, time: scheduleItem.time, coordinates: { - lon: this.waypoints[0].lon, - lat: this.waypoints[0].lat, + lon: this.originWaypoint.lon, + lat: this.originWaypoint.lat, }, }, this.frequency, @@ -177,11 +182,21 @@ export class MatchQuery extends QueryBase { const roles: Role[] = []; if (this.driver) roles.push(Role.DRIVER); if (this.passenger) roles.push(Role.PASSENGER); - const pathCreator: PathCreator = new PathCreator(roles, this.waypoints); + const pathCreator: PathCreator = new PathCreator( + roles, + this.waypoints.map( + (waypoint: Waypoint) => + new WaypointValueObject({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + }), + ), + ); try { ( await Promise.all( - pathCreator.getPaths().map(async (path: Path) => ({ + pathCreator.getBasePaths().map(async (path: Path) => ({ type: path.type, route: await routeProvider.getBasic(path.waypoints), })), diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 6da779e..3ebd818 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -1,5 +1,6 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { CandidateProps, CreateCandidateProps } from './candidate.types'; +import { WayStepProps } from './value-objects/waystep.value-object'; export class CandidateEntity extends AggregateRoot { protected readonly _id: AggregateID; @@ -9,6 +10,10 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; + setWaySteps = (waySteps: WayStepProps[]): void => { + this.props.waySteps = waySteps; + }; + validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 65e8cd6..7f48f73 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,20 +1,29 @@ import { Role } from './ad.types'; +import { WaypointProps } from './value-objects/waypoint.value-object'; +import { WayStepProps } from './value-objects/waystep.value-object'; // All properties that a Candidate has export interface CandidateProps { role: Role; - waypoints: Waypoint[]; + waypoints: WaypointProps[]; // waypoints of the original Ad + waySteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) } // Properties that are needed for a Candidate creation export interface CreateCandidateProps { id: string; role: Role; - waypoints: Waypoint[]; + waypoints: WaypointProps[]; } -export type Waypoint = { - lon: number; - lat: number; - position: number; +export type Spacetime = { + duration: number; + distance?: number; }; + +export enum Target { + START = 'START', + INTERMEDIATE = 'INTERMEDIATE', + FINISH = 'FINISH', + NEUTRAL = 'NEUTRAL', +} diff --git a/src/modules/ad/core/domain/patch-creator.service.ts b/src/modules/ad/core/domain/path-creator.service.ts similarity index 94% rename from src/modules/ad/core/domain/patch-creator.service.ts rename to src/modules/ad/core/domain/path-creator.service.ts index 4ac4668..a682173 100644 --- a/src/modules/ad/core/domain/patch-creator.service.ts +++ b/src/modules/ad/core/domain/path-creator.service.ts @@ -1,6 +1,6 @@ import { Route } from '@modules/geography/core/domain/route.types'; import { Role } from './ad.types'; -import { Waypoint } from './candidate.types'; +import { Waypoint } from './value-objects/waypoint.value-object'; export class PathCreator { constructor( @@ -8,7 +8,7 @@ export class PathCreator { private readonly waypoints: Waypoint[], ) {} - public getPaths = (): Path[] => { + public getBasePaths = (): Path[] => { const paths: Path[] = []; if ( this.roles.includes(Role.DRIVER) && diff --git a/src/modules/ad/core/domain/value-objects/actor.value-object.ts b/src/modules/ad/core/domain/value-objects/actor.value-object.ts new file mode 100644 index 0000000..d73c260 --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/actor.value-object.ts @@ -0,0 +1,27 @@ +import { ValueObject } from '@mobicoop/ddd-library'; +import { Role } from '../ad.types'; +import { Target } from '../candidate.types'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface ActorProps { + role: Role; + target: Target; +} + +export class Actor extends ValueObject { + get role(): Role { + return this.props.role; + } + + get target(): Target { + return this.props.target; + } + + protected validate(): void { + return; + } +} diff --git a/src/modules/ad/core/domain/value-objects/waypoint.value-object.ts b/src/modules/ad/core/domain/value-objects/waypoint.value-object.ts index 353f51d..d18fe04 100644 --- a/src/modules/ad/core/domain/value-objects/waypoint.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/waypoint.value-object.ts @@ -1,18 +1,13 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, - ValueObject, -} from '@mobicoop/ddd-library'; +import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; +import { PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface WaypointProps { +export interface WaypointProps extends PointProps { position: number; - lon: number; - lat: number; } export class Waypoint extends ValueObject { @@ -33,9 +28,5 @@ export class Waypoint extends ValueObject { throw new ArgumentInvalidException( 'position must be greater than or equal to 0', ); - if (props.lon > 180 || props.lon < -180) - throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); - if (props.lat > 90 || props.lat < -90) - throw new ArgumentOutOfRangeException('lat must be between -90 and 90'); } } diff --git a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts new file mode 100644 index 0000000..306f3f9 --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts @@ -0,0 +1,46 @@ +import { + ArgumentOutOfRangeException, + ValueObject, +} from '@mobicoop/ddd-library'; +import { WaypointProps } from './waypoint.value-object'; +import { Actor } from './actor.value-object'; +import { Role } from '../ad.types'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface WayStepProps extends WaypointProps { + actors: Actor[]; +} + +export class WayStep extends ValueObject { + get position(): number { + return this.props.position; + } + + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; + } + + get actors(): Actor[] { + return this.props.actors; + } + + protected validate(props: WayStepProps): void { + if (props.actors.length <= 0) + throw new ArgumentOutOfRangeException('at least one actor is required'); + if ( + props.actors.filter((actor: Actor) => actor.role == Role.DRIVER).length > + 1 + ) + throw new ArgumentOutOfRangeException( + 'a waystep can contain only one driver', + ); + } +} diff --git a/src/modules/ad/core/domain/waysteps-creator.service.ts b/src/modules/ad/core/domain/waysteps-creator.service.ts new file mode 100644 index 0000000..1d9fb05 --- /dev/null +++ b/src/modules/ad/core/domain/waysteps-creator.service.ts @@ -0,0 +1,62 @@ +import { Role } from './ad.types'; +import { Target } from './candidate.types'; +import { Actor } from './value-objects/actor.value-object'; +import { Waypoint } from './value-objects/waypoint.value-object'; +import { WayStep } from './value-objects/waystep.value-object'; + +export class WayStepsCreator { + constructor( + private readonly driverWaypoints: Waypoint[], + private readonly passengerWaypoints: Waypoint[], + ) {} + + public getCrewCarpoolPath = (): WayStep[] => this._createPassengerWaysteps(); + + private _createPassengerWaysteps = (): WayStep[] => { + const waysteps: WayStep[] = []; + this.passengerWaypoints.forEach((passengerWaypoint: Waypoint) => { + const waystep: WayStep = new WayStep({ + lon: passengerWaypoint.lon, + lat: passengerWaypoint.lat, + position: passengerWaypoint.position, + actors: [ + new Actor({ + role: Role.PASSENGER, + target: this._getTarget( + passengerWaypoint.position, + this.passengerWaypoints, + ), + }), + ], + }); + if ( + this.driverWaypoints.filter((driverWaypoint: Waypoint) => + this._isSameWaypoint(driverWaypoint, passengerWaypoint), + ).length > 0 + ) { + waystep.actors.push( + new Actor({ + role: Role.DRIVER, + target: Target.NEUTRAL, + }), + ); + } + waysteps.push(waystep); + }); + return waysteps; + }; + + private _isSameWaypoint = ( + waypoint1: Waypoint, + waypoint2: Waypoint, + ): boolean => + waypoint1.lon === waypoint2.lon && waypoint1.lat === waypoint2.lat; + + private _getTarget = (position: number, waypoints: Waypoint[]): Target => + position == 0 + ? Target.START + : position == + Math.max(...waypoints.map((waypoint: Waypoint) => waypoint.position)) + ? Target.FINISH + : Target.INTERMEDIATE; +} diff --git a/src/modules/ad/infrastructure/route-provider.ts b/src/modules/ad/infrastructure/route-provider.ts index 52a0189..5a7b1b0 100644 --- a/src/modules/ad/infrastructure/route-provider.ts +++ b/src/modules/ad/infrastructure/route-provider.ts @@ -1,9 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { RouteProviderPort } from '../core/application/ports/route-provider.port'; -import { Waypoint } from '../core/application/types/waypoint.type'; import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; -import { Route } from '@modules/geography/core/domain/route.types'; +import { Route, Waypoint } from '@modules/geography/core/domain/route.types'; @Injectable() export class RouteProvider implements RouteProviderPort { diff --git a/src/modules/ad/tests/unit/core/actor.value-object.spec.ts b/src/modules/ad/tests/unit/core/actor.value-object.spec.ts new file mode 100644 index 0000000..9e4c473 --- /dev/null +++ b/src/modules/ad/tests/unit/core/actor.value-object.spec.ts @@ -0,0 +1,14 @@ +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; + +describe('Actor value object', () => { + it('should create an actor value object', () => { + const actorVO = new Actor({ + role: Role.DRIVER, + target: Target.START, + }); + expect(actorVO.role).toBe(Role.DRIVER); + expect(actorVO.target).toBe(Target.START); + }); +}); diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 5f99b43..d02303c 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -40,6 +40,16 @@ const mockAdRepository = { id: 'cc260669-1c6d-441f-80a5-19cd59afb777', getProps: jest.fn().mockImplementation(() => ({ role: Role.DRIVER, + waypoints: [ + { + lat: 48.68787, + lon: 6.165871, + }, + { + lat: 48.97878, + lon: 2.45787, + }, + ], })), }, ]), diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts index 5900ae5..9700b77 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts @@ -63,13 +63,13 @@ const candidates: CandidateEntity[] = [ waypoints: [ { position: 0, - lat: 48.668487, - lon: 6.178457, + lat: 48.689445, + lon: 6.17651, }, { position: 1, - lat: 48.897457, - lon: 2.3688487, + lat: 48.8566, + lon: 2.3522, }, ], }), diff --git a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts index 6a9c693..d2121a9 100644 --- a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts @@ -1,26 +1,31 @@ import { Role } from '@modules/ad/core/domain/ad.types'; -import { Waypoint } from '@modules/ad/core/domain/candidate.types'; import { Path, PathCreator, PathType, -} from '@modules/ad/core/domain/patch-creator.service'; +} from '@modules/ad/core/domain/path-creator.service'; +import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; -const originWaypoint: Waypoint = { +const originWaypoint: Waypoint = new Waypoint({ position: 0, lat: 48.689445, lon: 6.17651, -}; -const destinationWaypoint: Waypoint = { +}); +const destinationWaypoint: Waypoint = new Waypoint({ position: 1, lat: 48.8566, lon: 2.3522, -}; -const intermediateWaypoint: Waypoint = { +}); +const intermediateWaypoint: Waypoint = new Waypoint({ position: 1, lat: 48.74488, lon: 4.8972, -}; +}); +const destinationWaypointWithIntermediateWaypoint: Waypoint = new Waypoint({ + position: 2, + lat: 48.8566, + lon: 2.3522, +}); describe('Path Creator Service', () => { it('should create a path for a driver only', () => { @@ -28,7 +33,7 @@ describe('Path Creator Service', () => { [Role.DRIVER], [originWaypoint, destinationWaypoint], ); - const paths: Path[] = pathCreator.getPaths(); + const paths: Path[] = pathCreator.getBasePaths(); expect(paths).toHaveLength(1); expect(paths[0].type).toBe(PathType.DRIVER); }); @@ -37,7 +42,7 @@ describe('Path Creator Service', () => { [Role.PASSENGER], [originWaypoint, destinationWaypoint], ); - const paths: Path[] = pathCreator.getPaths(); + const paths: Path[] = pathCreator.getBasePaths(); expect(paths).toHaveLength(1); expect(paths[0].type).toBe(PathType.PASSENGER); }); @@ -46,7 +51,7 @@ describe('Path Creator Service', () => { [Role.DRIVER, Role.PASSENGER], [originWaypoint, destinationWaypoint], ); - const paths: Path[] = pathCreator.getPaths(); + const paths: Path[] = pathCreator.getBasePaths(); expect(paths).toHaveLength(1); expect(paths[0].type).toBe(PathType.GENERIC); }); @@ -56,10 +61,10 @@ describe('Path Creator Service', () => { [ originWaypoint, intermediateWaypoint, - { ...destinationWaypoint, position: 2 }, + destinationWaypointWithIntermediateWaypoint, ], ); - const paths: Path[] = pathCreator.getPaths(); + const paths: Path[] = pathCreator.getBasePaths(); expect(paths).toHaveLength(2); expect( paths.filter((path: Path) => path.type == PathType.DRIVER), diff --git a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts new file mode 100644 index 0000000..472cdb4 --- /dev/null +++ b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts @@ -0,0 +1,62 @@ +import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; +import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; + +describe('WayStep value object', () => { + it('should create a waystep value object', () => { + const wayStepVO = new WayStep({ + lat: 48.689445, + lon: 6.17651, + position: 0, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.NEUTRAL, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.START, + }), + ], + }); + expect(wayStepVO.position).toBe(0); + expect(wayStepVO.lon).toBe(6.17651); + expect(wayStepVO.lat).toBe(48.689445); + expect(wayStepVO.actors).toHaveLength(2); + }); + it('should throw an exception if actors is empty', () => { + try { + new WayStep({ + lat: 48.689445, + lon: 6.17651, + position: 0, + actors: [], + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + }); + it('should throw an exception if actors contains more than one driver', () => { + try { + new WayStep({ + lat: 48.689445, + lon: 6.17651, + position: 0, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.NEUTRAL, + }), + new Actor({ + role: Role.DRIVER, + target: Target.START, + }), + ], + }); + } catch (e: any) { + expect(e).toBeInstanceOf(ArgumentOutOfRangeException); + } + }); +}); diff --git a/src/modules/geography/core/application/queries/get-route/get-route.query.ts b/src/modules/geography/core/application/queries/get-route/get-route.query.ts index 0c936d9..6697942 100644 --- a/src/modules/geography/core/application/queries/get-route/get-route.query.ts +++ b/src/modules/geography/core/application/queries/get-route/get-route.query.ts @@ -1,6 +1,6 @@ import { QueryBase } from '@mobicoop/ddd-library'; -import { Waypoint } from '@modules/geography/core/domain/route.types'; import { GeorouterSettings } from '../../types/georouter-settings.type'; +import { Waypoint } from '@modules/geography/core/domain/route.types'; export class GetRouteQuery extends QueryBase { readonly waypoints: Waypoint[]; diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index e6df76c..e6ff203 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -20,6 +20,7 @@ export interface CreateRouteProps { georouterSettings: GeorouterSettings; } +// Types used outside the domain export type Route = { distance: number; duration: number; diff --git a/src/modules/geography/core/domain/value-objects/step.value-object.ts b/src/modules/geography/core/domain/value-objects/step.value-object.ts index a5c180f..5c04757 100644 --- a/src/modules/geography/core/domain/value-objects/step.value-object.ts +++ b/src/modules/geography/core/domain/value-objects/step.value-object.ts @@ -1,30 +1,17 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, - ValueObject, -} from '@mobicoop/ddd-library'; +import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; +import { PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface StepProps { - lon: number; - lat: number; +export interface StepProps extends PointProps { duration: number; distance: number; } export class Step extends ValueObject { - get lon(): number { - return this.props.lon; - } - - get lat(): number { - return this.props.lat; - } - get duration(): number { return this.props.duration; } @@ -33,6 +20,14 @@ export class Step extends ValueObject { return this.props.distance; } + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; + } + protected validate(props: StepProps): void { if (props.duration < 0) throw new ArgumentInvalidException( @@ -42,9 +37,5 @@ export class Step extends ValueObject { throw new ArgumentInvalidException( 'distance must be greater than or equal to 0', ); - if (props.lon > 180 || props.lon < -180) - throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); - if (props.lat > 90 || props.lat < -90) - throw new ArgumentOutOfRangeException('lat must be between -90 and 90'); } } diff --git a/src/modules/geography/core/domain/value-objects/waypoint.value-object.ts b/src/modules/geography/core/domain/value-objects/waypoint.value-object.ts index 353f51d..d18fe04 100644 --- a/src/modules/geography/core/domain/value-objects/waypoint.value-object.ts +++ b/src/modules/geography/core/domain/value-objects/waypoint.value-object.ts @@ -1,18 +1,13 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, - ValueObject, -} from '@mobicoop/ddd-library'; +import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; +import { PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface WaypointProps { +export interface WaypointProps extends PointProps { position: number; - lon: number; - lat: number; } export class Waypoint extends ValueObject { @@ -33,9 +28,5 @@ export class Waypoint extends ValueObject { throw new ArgumentInvalidException( 'position must be greater than or equal to 0', ); - if (props.lon > 180 || props.lon < -180) - throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); - if (props.lat > 90 || props.lat < -90) - throw new ArgumentOutOfRangeException('lat must be between -90 and 90'); } } From c65a5b50c2edaacdfecd5164fd41ec393a49cae7 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 13 Sep 2023 15:57:22 +0200 Subject: [PATCH 25/52] fix waystep completer --- ...pleter.ts => passenger-oriented-waysteps.completer.ts} | 7 +------ .../queries/match/passenger-oriented-algorithm.ts | 4 ++-- ...c.ts => passenger-oriented-waysteps-completer.spec.ts} | 8 ++++---- 3 files changed, 7 insertions(+), 12 deletions(-) rename src/modules/ad/core/application/queries/match/completer/{passenger-oriented-waypoints.completer.ts => passenger-oriented-waysteps.completer.ts} (91%) rename src/modules/ad/tests/unit/core/{passenger-oriented-waypoints-completer.spec.ts => passenger-oriented-waysteps-completer.spec.ts} (87%) diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer.ts similarity index 91% rename from src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts rename to src/modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer.ts index c281177..339c5a6 100644 --- a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer.ts @@ -11,7 +11,7 @@ import { WayStepsCreator } from '@modules/ad/core/domain/waysteps-creator.servic /** * Complete candidates by setting driver and crew waypoints */ -export class PassengerOrientedWaypointsCompleter extends Completer { +export class PassengerOrientedWayStepsCompleter extends Completer { complete = async ( candidates: CandidateEntity[], ): Promise => { @@ -54,11 +54,6 @@ export class PassengerOrientedWaypointsCompleter extends Completer { ); candidate.setWaySteps(carpoolPathCreator.getCrewCarpoolPath()); }); - // console.log( - // candidates[0] - // .getProps() - // .waySteps?.map((waystep: WayStep) => waystep.actors), - // ); return candidates; }; } diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index 8ca7147..b06faf0 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -1,6 +1,6 @@ import { Algorithm } from './algorithm.abstract'; import { MatchQuery } from './match.query'; -import { PassengerOrientedWaypointsCompleter } from './completer/passenger-oriented-waypoints.completer'; +import { PassengerOrientedWayStepsCompleter } from './completer/passenger-oriented-waysteps.completer'; import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { PassengerOrientedSelector } from './selector/passenger-oriented.selector'; @@ -13,7 +13,7 @@ export class PassengerOrientedAlgorithm extends Algorithm { super(query, repository); this.selector = new PassengerOrientedSelector(query, repository); this.processors = [ - new PassengerOrientedWaypointsCompleter(query), + new PassengerOrientedWayStepsCompleter(query), new PassengerOrientedGeoFilter(query), ]; } diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-waysteps-completer.spec.ts similarity index 87% rename from src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts rename to src/modules/ad/tests/unit/core/passenger-oriented-waysteps-completer.spec.ts index 9700b77..d840b7e 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-waypoints-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-waysteps-completer.spec.ts @@ -1,4 +1,4 @@ -import { PassengerOrientedWaypointsCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer'; +import { PassengerOrientedWayStepsCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; @@ -75,10 +75,10 @@ const candidates: CandidateEntity[] = [ }), ]; -describe('Passenger oriented waypoints completer', () => { +describe('Passenger oriented waysteps completer', () => { it('should complete candidates', async () => { - const passengerOrientedWaypointsCompleter: PassengerOrientedWaypointsCompleter = - new PassengerOrientedWaypointsCompleter(matchQuery); + const passengerOrientedWaypointsCompleter: PassengerOrientedWayStepsCompleter = + new PassengerOrientedWayStepsCompleter(matchQuery); const completedCandidates: CandidateEntity[] = await passengerOrientedWaypointsCompleter.complete(candidates); expect(completedCandidates.length).toBe(2); From f69afc4481d087b7cdd2a1f7fa4152f9441cafd3 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 14 Sep 2023 17:07:38 +0200 Subject: [PATCH 26/52] remove waypoints where not relevant --- .../commands/create-ad/create-ad.command.ts | 4 +- .../commands/create-ad/create-ad.service.ts | 26 +++++-- .../application/ports/route-provider.port.ts | 4 +- ...senger-oriented-carpool-path.completer.ts} | 39 +++++----- .../application/queries/match/match.query.ts | 5 +- .../match/passenger-oriented-algorithm.ts | 4 +- .../selector/passenger-oriented.selector.ts | 31 +++++++- .../core/application/types/algorithm.types.ts | 6 +- src/modules/ad/core/domain/ad.types.ts | 5 +- .../ad/core/domain/candidate.entity.ts | 4 +- src/modules/ad/core/domain/candidate.types.ts | 14 +++- .../domain/carpool-path-creator.service.ts | 76 +++++++++++++++++++ .../ad/core/domain/path-creator.service.ts | 23 ++---- .../value-objects/waypoint.value-object.ts | 32 -------- .../value-objects/waystep.value-object.ts | 8 +- .../core/domain/waysteps-creator.service.ts | 62 --------------- .../ad/infrastructure/ad.repository.ts | 2 +- .../ad/infrastructure/route-provider.ts | 4 +- src/modules/ad/tests/unit/ad.mapper.spec.ts | 2 - .../ad/tests/unit/core/ad.entity.spec.ts | 10 +-- .../tests/unit/core/create-ad.service.spec.ts | 8 +- ...r-oriented-carpool-path-completer.spec.ts} | 50 ++++++++---- .../passenger-oriented-geo-filter.spec.ts | 40 +++++++--- .../unit/core/path-creator.service.spec.ts | 22 ++---- .../unit/core/waypoint.value-object.spec.ts | 69 ----------------- .../unit/core/waystep.value-object.spec.ts | 4 - .../infrastructure/route-provider.spec.ts | 10 +-- .../core/application/ports/georouter.port.ts | 4 +- .../queries/get-route/get-route.query.ts | 6 +- .../geography/core/domain/route.types.ts | 11 +-- .../value-objects/waypoint.value-object.ts | 32 -------- .../infrastructure/graphhopper-georouter.ts | 8 +- .../controllers/dtos/get-route.request.dto.ts | 4 +- .../unit/core/get-route.query-handler.spec.ts | 8 +- .../tests/unit/core/route.entity.spec.ts | 11 +-- .../unit/core/waypoint.value-object.spec.ts | 69 ----------------- .../graphhopper-georouter.spec.ts | 15 ---- .../get-basic-route.controller.spec.ts | 2 - 38 files changed, 273 insertions(+), 461 deletions(-) rename src/modules/ad/core/application/queries/match/completer/{passenger-oriented-waysteps.completer.ts => passenger-oriented-carpool-path.completer.ts} (58%) create mode 100644 src/modules/ad/core/domain/carpool-path-creator.service.ts delete mode 100644 src/modules/ad/core/domain/value-objects/waypoint.value-object.ts delete mode 100644 src/modules/ad/core/domain/waysteps-creator.service.ts rename src/modules/ad/tests/unit/core/{passenger-oriented-waysteps-completer.spec.ts => passenger-oriented-carpool-path-completer.spec.ts} (65%) delete mode 100644 src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts delete mode 100644 src/modules/geography/core/domain/value-objects/waypoint.value-object.ts delete mode 100644 src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts index 5d9839f..a3a6d9a 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts @@ -1,7 +1,7 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; import { Command, CommandProps } from '@mobicoop/ddd-library'; import { ScheduleItem } from '../../types/schedule-item.type'; -import { Waypoint } from '../../types/waypoint.type'; +import { Address } from '../../types/address.type'; export class CreateAdCommand extends Command { readonly id: string; @@ -14,7 +14,7 @@ export class CreateAdCommand extends Command { readonly seatsProposed: number; readonly seatsRequested: number; readonly strict: boolean; - readonly waypoints: Waypoint[]; + readonly waypoints: Address[]; constructor(props: CommandProps) { super(props); diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 380de24..1f7c55e 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -14,9 +14,9 @@ import { PathType, TypedRoute, } from '@modules/ad/core/domain/path-creator.service'; -import { Point } from '../../types/point.type'; import { Waypoint } from '../../types/waypoint.type'; -import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { Point as PointValueObject } from '@modules/ad/core/domain/value-objects/point.value-object'; +import { Point } from '@modules/geography/core/domain/route.types'; @CommandHandler(CreateAdCommand) export class CreateAdService implements ICommandHandler { @@ -35,8 +35,7 @@ export class CreateAdService implements ICommandHandler { roles, command.waypoints.map( (waypoint: Waypoint) => - new WaypointValueObject({ - position: waypoint.position, + new PointValueObject({ lon: waypoint.lon, lat: waypoint.lat, }), @@ -58,21 +57,34 @@ export class CreateAdService implements ICommandHandler { let driverDuration: number | undefined; let passengerDistance: number | undefined; let passengerDuration: number | undefined; - let points: Point[] | undefined; + let points: PointValueObject[] | undefined; let fwdAzimuth: number | undefined; let backAzimuth: number | undefined; typedRoutes.forEach((typedRoute: TypedRoute) => { if (typedRoute.type !== PathType.PASSENGER) { driverDistance = typedRoute.route.distance; driverDuration = typedRoute.route.duration; - points = typedRoute.route.points; + points = typedRoute.route.points.map( + (point: Point) => + new PointValueObject({ + lon: point.lon, + lat: point.lat, + }), + ); fwdAzimuth = typedRoute.route.fwdAzimuth; backAzimuth = typedRoute.route.backAzimuth; } if (typedRoute.type !== PathType.DRIVER) { passengerDistance = typedRoute.route.distance; passengerDuration = typedRoute.route.duration; - if (!points) points = typedRoute.route.points; + if (!points) + points = typedRoute.route.points.map( + (point: Point) => + new PointValueObject({ + lon: point.lon, + lat: point.lat, + }), + ); if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; } diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts index 7d86496..b016365 100644 --- a/src/modules/ad/core/application/ports/route-provider.port.ts +++ b/src/modules/ad/core/application/ports/route-provider.port.ts @@ -1,9 +1,9 @@ import { Route } from '@modules/geography/core/domain/route.types'; -import { Waypoint } from '../../domain/value-objects/waypoint.value-object'; +import { Point } from '../types/point.type'; export interface RouteProviderPort { /** * Get a basic route with points and overall duration / distance */ - getBasic(waypoints: Waypoint[]): Promise; + getBasic(waypoints: Point[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts similarity index 58% rename from src/modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer.ts rename to src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts index 339c5a6..ccba94c 100644 --- a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts @@ -1,58 +1,55 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Completer } from './completer.abstract'; import { Role } from '@modules/ad/core/domain/ad.types'; -import { - Waypoint as WaypointValueObject, - WaypointProps, -} from '@modules/ad/core/domain/value-objects/waypoint.value-object'; import { Waypoint } from '../../../types/waypoint.type'; -import { WayStepsCreator } from '@modules/ad/core/domain/waysteps-creator.service'; +import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; +import { + Point, + PointProps, +} from '@modules/ad/core/domain/value-objects/point.value-object'; /** - * Complete candidates by setting driver and crew waypoints + * Complete candidates with crew carpool path */ -export class PassengerOrientedWayStepsCompleter extends Completer { +export class PassengerOrientedCarpoolPathCompleter extends Completer { complete = async ( candidates: CandidateEntity[], ): Promise => { candidates.forEach((candidate: CandidateEntity) => { - const carpoolPathCreator = new WayStepsCreator( + const carpoolPathCreator = new CarpoolPathCreator( candidate.getProps().role == Role.DRIVER - ? candidate.getProps().waypoints.map( - (waypoint: WaypointProps) => - new WaypointValueObject({ - position: waypoint.position, + ? candidate.getProps().driverWaypoints.map( + (waypoint: PointProps) => + new Point({ lon: waypoint.lon, lat: waypoint.lat, }), ) : this.query.waypoints.map( (waypoint: Waypoint) => - new WaypointValueObject({ - position: waypoint.position, + new Point({ lon: waypoint.lon, lat: waypoint.lat, }), ), candidate.getProps().role == Role.PASSENGER - ? candidate.getProps().waypoints.map( - (waypoint: WaypointProps) => - new WaypointValueObject({ - position: waypoint.position, + ? candidate.getProps().driverWaypoints.map( + (waypoint: PointProps) => + new Point({ lon: waypoint.lon, lat: waypoint.lat, }), ) : this.query.waypoints.map( (waypoint: Waypoint) => - new WaypointValueObject({ - position: waypoint.position, + new Point({ lon: waypoint.lon, lat: waypoint.lat, }), ), ); - candidate.setWaySteps(carpoolPathCreator.getCrewCarpoolPath()); + candidate.setCarpoolPath(carpoolPathCreator.createCarpoolPath()); + console.log(JSON.stringify(candidate, null, 2)); }); return candidates; }; diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 55a31b6..3fb9a4d 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -12,7 +12,7 @@ import { PathType, TypedRoute, } from '@modules/ad/core/domain/path-creator.service'; -import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; export class MatchQuery extends QueryBase { driver?: boolean; @@ -186,8 +186,7 @@ export class MatchQuery extends QueryBase { roles, this.waypoints.map( (waypoint: Waypoint) => - new WaypointValueObject({ - position: waypoint.position, + new Point({ lon: waypoint.lon, lat: waypoint.lat, }), diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index b06faf0..dc0642b 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -1,6 +1,6 @@ import { Algorithm } from './algorithm.abstract'; import { MatchQuery } from './match.query'; -import { PassengerOrientedWayStepsCompleter } from './completer/passenger-oriented-waysteps.completer'; +import { PassengerOrientedCarpoolPathCompleter } from './completer/passenger-oriented-carpool-path.completer'; import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { PassengerOrientedSelector } from './selector/passenger-oriented.selector'; @@ -13,7 +13,7 @@ export class PassengerOrientedAlgorithm extends Algorithm { super(query, repository); this.selector = new PassengerOrientedSelector(query, repository); this.processors = [ - new PassengerOrientedWayStepsCompleter(query), + new PassengerOrientedCarpoolPathCompleter(query), new PassengerOrientedGeoFilter(query), ]; } diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index dea1679..7127eaa 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -35,8 +35,31 @@ export class PassengerOrientedSelector extends Selector { adsRole.ads.map((adEntity: AdEntity) => CandidateEntity.create({ id: adEntity.id, - role: adsRole.role, - waypoints: adEntity.getProps().waypoints, + role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER, + driverWaypoints: + adsRole.role == Role.PASSENGER + ? adEntity.getProps().waypoints + : this.query.waypoints.map((waypoint: Waypoint) => ({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + })), + passengerWaypoints: + adsRole.role == Role.DRIVER + ? adEntity.getProps().waypoints + : this.query.waypoints.map((waypoint: Waypoint) => ({ + position: waypoint.position, + lon: waypoint.lon, + lat: waypoint.lat, + })), + driverDistance: + adsRole.role == Role.PASSENGER + ? (adEntity.getProps().driverDistance as number) + : (this.query.driverRoute?.distance as number), + driverDuration: + adsRole.role == Role.PASSENGER + ? (adEntity.getProps().driverDuration as number) + : (this.query.driverRoute?.duration as number), }), ), ) @@ -67,10 +90,10 @@ export class PassengerOrientedSelector extends Selector { ].join(); private _selectAsDriver = (): string => - `${this.query.driverRoute?.duration} as duration,${this.query.driverRoute?.distance} as distance`; + `${this.query.driverRoute?.duration} as driverDuration,${this.query.driverRoute?.distance} as driverDistance`; private _selectAsPassenger = (): string => - `"driverDuration" as duration,"driverDistance" as distance`; + `"driverDuration","driverDistance"`; private _createFrom = (): string => 'FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid"'; diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts index 4d1fd81..dd5809d 100644 --- a/src/modules/ad/core/application/types/algorithm.types.ts +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -1,5 +1,5 @@ -import { Waypoint } from '@modules/geography/core/domain/route.types'; import { Role } from '../../domain/ad.types'; +import { Point } from '../../domain/value-objects/point.value-object'; export enum AlgorithmType { PASSENGER_ORIENTED = 'PASSENGER_ORIENTED', @@ -11,8 +11,8 @@ export enum AlgorithmType { export type Candidate = { ad: Ad; role: Role; - driverWaypoints: Waypoint[]; - crewWaypoints: Waypoint[]; + driverWaypoints: Point[]; + crewWaypoints: Point[]; }; export type Ad = { diff --git a/src/modules/ad/core/domain/ad.types.ts b/src/modules/ad/core/domain/ad.types.ts index ba60073..10f906b 100644 --- a/src/modules/ad/core/domain/ad.types.ts +++ b/src/modules/ad/core/domain/ad.types.ts @@ -1,6 +1,5 @@ import { PointProps } from './value-objects/point.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; -import { WaypointProps } from './value-objects/waypoint.value-object'; // All properties that an Ad has export interface AdProps { @@ -17,7 +16,7 @@ export interface AdProps { driverDistance?: number; passengerDuration?: number; passengerDistance?: number; - waypoints: WaypointProps[]; + waypoints: PointProps[]; points: PointProps[]; fwdAzimuth: number; backAzimuth: number; @@ -35,7 +34,7 @@ export interface CreateAdProps { seatsProposed: number; seatsRequested: number; strict: boolean; - waypoints: WaypointProps[]; + waypoints: PointProps[]; driverDuration?: number; driverDistance?: number; passengerDuration?: number; diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 3ebd818..3add4e3 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -10,8 +10,8 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; - setWaySteps = (waySteps: WayStepProps[]): void => { - this.props.waySteps = waySteps; + setCarpoolPath = (waySteps: WayStepProps[]): void => { + this.props.carpoolSteps = waySteps; }; validate(): void { diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 7f48f73..b86b09b 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,19 +1,25 @@ import { Role } from './ad.types'; -import { WaypointProps } from './value-objects/waypoint.value-object'; +import { PointProps } from './value-objects/point.value-object'; import { WayStepProps } from './value-objects/waystep.value-object'; // All properties that a Candidate has export interface CandidateProps { role: Role; - waypoints: WaypointProps[]; // waypoints of the original Ad - waySteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) + driverWaypoints: PointProps[]; + passengerWaypoints: PointProps[]; + driverDistance: number; + driverDuration: number; + carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) } // Properties that are needed for a Candidate creation export interface CreateCandidateProps { id: string; role: Role; - waypoints: WaypointProps[]; + driverDistance: number; + driverDuration: number; + driverWaypoints: PointProps[]; + passengerWaypoints: PointProps[]; } export type Spacetime = { diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts new file mode 100644 index 0000000..9af2fd9 --- /dev/null +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -0,0 +1,76 @@ +import { Role } from './ad.types'; +import { Target } from './candidate.types'; +import { Actor } from './value-objects/actor.value-object'; +import { Point } from './value-objects/point.value-object'; +import { WayStep } from './value-objects/waystep.value-object'; + +export class CarpoolPathCreator { + constructor( + private readonly driverWaypoints: Point[], + private readonly passengerWaypoints: Point[], + ) {} + + public createCarpoolPath = (): WayStep[] => { + const passengerWaysteps: WayStep[] = this._createPassengerWaysteps(); + if (this.driverWaypoints.length == 2) { + } + return passengerWaysteps; + }; + + private _createDriverWaysteps = (): WayStep[] => + this.driverWaypoints.map( + (waypoint: Point, index: number) => + new WayStep({ + lon: waypoint.lon, + lat: waypoint.lat, + actors: [ + new Actor({ + role: Role.DRIVER, + target: this._getTarget(index, this.driverWaypoints), + }), + ], + }), + ); + + private _createPassengerWaysteps = (): WayStep[] => { + const waysteps: WayStep[] = []; + this.passengerWaypoints.forEach( + (passengerWaypoint: Point, index: number) => { + const waystep: WayStep = new WayStep({ + lon: passengerWaypoint.lon, + lat: passengerWaypoint.lat, + actors: [ + new Actor({ + role: Role.PASSENGER, + target: this._getTarget(index, this.passengerWaypoints), + }), + ], + }); + if ( + this.driverWaypoints.filter((driverWaypoint: Point) => + this._isSamePoint(driverWaypoint, passengerWaypoint), + ).length > 0 + ) { + waystep.actors.push( + new Actor({ + role: Role.DRIVER, + target: Target.NEUTRAL, + }), + ); + } + waysteps.push(waystep); + }, + ); + return waysteps; + }; + + private _isSamePoint = (point1: Point, point2: Point): boolean => + point1.lon === point2.lon && point1.lat === point2.lat; + + private _getTarget = (index: number, waypoints: Point[]): Target => + index == 0 + ? Target.START + : index == waypoints.length - 1 + ? Target.FINISH + : Target.INTERMEDIATE; +} diff --git a/src/modules/ad/core/domain/path-creator.service.ts b/src/modules/ad/core/domain/path-creator.service.ts index a682173..f44d147 100644 --- a/src/modules/ad/core/domain/path-creator.service.ts +++ b/src/modules/ad/core/domain/path-creator.service.ts @@ -1,11 +1,11 @@ import { Route } from '@modules/geography/core/domain/route.types'; import { Role } from './ad.types'; -import { Waypoint } from './value-objects/waypoint.value-object'; +import { Point } from './value-objects/point.value-object'; export class PathCreator { constructor( private readonly roles: Role[], - private readonly waypoints: Waypoint[], + private readonly waypoints: Point[], ) {} public getBasePaths = (): Path[] => { @@ -40,21 +40,12 @@ export class PathCreator { PathType.PASSENGER, ); - private _firstWaypoint = (): Waypoint => - this.waypoints.find( - (waypoint: Waypoint) => waypoint.position == 0, - ) as Waypoint; + private _firstWaypoint = (): Point => this.waypoints[0]; - private _lastWaypoint = (): Waypoint => - this.waypoints.find( - (waypoint: Waypoint) => - waypoint.position == - Math.max( - ...this.waypoints.map((waypoint: Waypoint) => waypoint.position), - ), - ) as Waypoint; + private _lastWaypoint = (): Point => + this.waypoints[this.waypoints.length - 1]; - private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({ + private _createPath = (waypoints: Point[], type: PathType): Path => ({ type, waypoints, }); @@ -62,7 +53,7 @@ export class PathCreator { export type Path = { type: PathType; - waypoints: Waypoint[]; + waypoints: Point[]; }; export type TypedRoute = { diff --git a/src/modules/ad/core/domain/value-objects/waypoint.value-object.ts b/src/modules/ad/core/domain/value-objects/waypoint.value-object.ts deleted file mode 100644 index d18fe04..0000000 --- a/src/modules/ad/core/domain/value-objects/waypoint.value-object.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; -import { PointProps } from './point.value-object'; - -/** Note: - * Value Objects with multiple properties can contain - * other Value Objects inside if needed. - * */ - -export interface WaypointProps extends PointProps { - position: number; -} - -export class Waypoint extends ValueObject { - get position(): number { - return this.props.position; - } - - get lon(): number { - return this.props.lon; - } - - get lat(): number { - return this.props.lat; - } - - protected validate(props: WaypointProps): void { - if (props.position < 0) - throw new ArgumentInvalidException( - 'position must be greater than or equal to 0', - ); - } -} diff --git a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts index 306f3f9..7797525 100644 --- a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts @@ -2,24 +2,20 @@ import { ArgumentOutOfRangeException, ValueObject, } from '@mobicoop/ddd-library'; -import { WaypointProps } from './waypoint.value-object'; import { Actor } from './actor.value-object'; import { Role } from '../ad.types'; +import { PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface WayStepProps extends WaypointProps { +export interface WayStepProps extends PointProps { actors: Actor[]; } export class WayStep extends ValueObject { - get position(): number { - return this.props.position; - } - get lon(): number { return this.props.lon; } diff --git a/src/modules/ad/core/domain/waysteps-creator.service.ts b/src/modules/ad/core/domain/waysteps-creator.service.ts deleted file mode 100644 index 1d9fb05..0000000 --- a/src/modules/ad/core/domain/waysteps-creator.service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Role } from './ad.types'; -import { Target } from './candidate.types'; -import { Actor } from './value-objects/actor.value-object'; -import { Waypoint } from './value-objects/waypoint.value-object'; -import { WayStep } from './value-objects/waystep.value-object'; - -export class WayStepsCreator { - constructor( - private readonly driverWaypoints: Waypoint[], - private readonly passengerWaypoints: Waypoint[], - ) {} - - public getCrewCarpoolPath = (): WayStep[] => this._createPassengerWaysteps(); - - private _createPassengerWaysteps = (): WayStep[] => { - const waysteps: WayStep[] = []; - this.passengerWaypoints.forEach((passengerWaypoint: Waypoint) => { - const waystep: WayStep = new WayStep({ - lon: passengerWaypoint.lon, - lat: passengerWaypoint.lat, - position: passengerWaypoint.position, - actors: [ - new Actor({ - role: Role.PASSENGER, - target: this._getTarget( - passengerWaypoint.position, - this.passengerWaypoints, - ), - }), - ], - }); - if ( - this.driverWaypoints.filter((driverWaypoint: Waypoint) => - this._isSameWaypoint(driverWaypoint, passengerWaypoint), - ).length > 0 - ) { - waystep.actors.push( - new Actor({ - role: Role.DRIVER, - target: Target.NEUTRAL, - }), - ); - } - waysteps.push(waystep); - }); - return waysteps; - }; - - private _isSameWaypoint = ( - waypoint1: Waypoint, - waypoint2: Waypoint, - ): boolean => - waypoint1.lon === waypoint2.lon && waypoint1.lat === waypoint2.lat; - - private _getTarget = (position: number, waypoints: Waypoint[]): Target => - position == 0 - ? Target.START - : position == - Math.max(...waypoints.map((waypoint: Waypoint) => waypoint.position)) - ? Target.FINISH - : Target.INTERMEDIATE; -} diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index d5b0f24..5f45287 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -30,7 +30,7 @@ export type AdModel = { }; /** - * The record as returned by the peristence system + * The record as returned by the persistence system */ export type AdReadModel = AdModel & { waypoints: string; diff --git a/src/modules/ad/infrastructure/route-provider.ts b/src/modules/ad/infrastructure/route-provider.ts index 5a7b1b0..b32317b 100644 --- a/src/modules/ad/infrastructure/route-provider.ts +++ b/src/modules/ad/infrastructure/route-provider.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { RouteProviderPort } from '../core/application/ports/route-provider.port'; import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; -import { Route, Waypoint } from '@modules/geography/core/domain/route.types'; +import { Point, Route } from '@modules/geography/core/domain/route.types'; @Injectable() export class RouteProvider implements RouteProviderPort { @@ -11,7 +11,7 @@ export class RouteProvider implements RouteProviderPort { private readonly getBasicRouteController: GetBasicRouteControllerPort, ) {} - getBasic = async (waypoints: Waypoint[]): Promise => + getBasic = async (waypoints: Point[]): Promise => await this.getBasicRouteController.get({ waypoints, }); diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index 7e93238..6c2a662 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -28,12 +28,10 @@ const adEntity: AdEntity = new AdEntity({ ], waypoints: [ { - position: 0, lat: 48.689445, lon: 6.1765102, }, { - position: 1, lat: 48.8566, lon: 2.3522, }, diff --git a/src/modules/ad/tests/unit/core/ad.entity.spec.ts b/src/modules/ad/tests/unit/core/ad.entity.spec.ts index 8b04836..6a8fe97 100644 --- a/src/modules/ad/tests/unit/core/ad.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/ad.entity.spec.ts @@ -1,14 +1,12 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; -import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object'; -const originWaypointProps: WaypointProps = { - position: 0, +const originPointProps: PointProps = { lat: 48.689445, lon: 6.17651, }; -const destinationWaypointProps: WaypointProps = { - position: 1, +const destinationPointProps: PointProps = { lat: 48.8566, lon: 2.3522, }; @@ -30,7 +28,7 @@ const createAdProps: CreateAdProps = { seatsProposed: 3, seatsRequested: 1, strict: false, - waypoints: [originWaypointProps, destinationWaypointProps], + waypoints: [originPointProps, destinationPointProps], driverDistance: 23000, driverDuration: 900, passengerDistance: 23000, diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 6db752e..4ec978c 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -7,16 +7,14 @@ import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service'; import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; -import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object'; -const originWaypoint: WaypointProps = { - position: 0, +const originWaypoint: PointProps = { lat: 48.689445, lon: 6.17651, }; -const destinationWaypoint: WaypointProps = { - position: 1, +const destinationWaypoint: PointProps = { lat: 48.8566, lon: 2.3522, }; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-waysteps-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts similarity index 65% rename from src/modules/ad/tests/unit/core/passenger-oriented-waysteps-completer.spec.ts rename to src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index d840b7e..9d607b7 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-waysteps-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -1,4 +1,4 @@ -import { PassengerOrientedWayStepsCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-waysteps.completer'; +import { PassengerOrientedCarpoolPathCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; @@ -44,43 +44,63 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, - waypoints: [ + driverWaypoints: [ { - position: 0, lat: 48.678454, lon: 6.189745, }, { - position: 1, lat: 48.84877, lon: 2.398457, }, ], - }), - CandidateEntity.create({ - id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', - role: Role.PASSENGER, - waypoints: [ + passengerWaypoints: [ { - position: 0, lat: 48.689445, lon: 6.17651, }, { - position: 1, lat: 48.8566, lon: 2.3522, }, ], + driverDistance: 350145, + driverDuration: 13548, + }), + CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + role: Role.PASSENGER, + driverWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, }), ]; -describe('Passenger oriented waysteps completer', () => { +describe('Passenger oriented carpool path completer', () => { it('should complete candidates', async () => { - const passengerOrientedWaypointsCompleter: PassengerOrientedWayStepsCompleter = - new PassengerOrientedWayStepsCompleter(matchQuery); + const passengerOrientedCarpoolPathCompleter: PassengerOrientedCarpoolPathCompleter = + new PassengerOrientedCarpoolPathCompleter(matchQuery); const completedCandidates: CandidateEntity[] = - await passengerOrientedWaypointsCompleter.complete(candidates); + await passengerOrientedCarpoolPathCompleter.complete(candidates); expect(completedCandidates.length).toBe(2); }); }); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 27b497b..51770d9 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -44,34 +44,54 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, - waypoints: [ + driverWaypoints: [ { - position: 0, lat: 48.678454, lon: 6.189745, }, { - position: 1, lat: 48.84877, lon: 2.398457, }, ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, }), CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, - waypoints: [ + driverWaypoints: [ { - position: 0, - lat: 48.668487, - lon: 6.178457, + lat: 48.689445, + lon: 6.17651, }, { - position: 1, - lat: 48.897457, - lon: 2.3688487, + lat: 48.8566, + lon: 2.3522, }, ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, }), ]; diff --git a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts index d2121a9..dfb85f1 100644 --- a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts @@ -4,28 +4,20 @@ import { PathCreator, PathType, } from '@modules/ad/core/domain/path-creator.service'; -import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; -const originWaypoint: Waypoint = new Waypoint({ - position: 0, +const originWaypoint: Point = new Point({ lat: 48.689445, lon: 6.17651, }); -const destinationWaypoint: Waypoint = new Waypoint({ - position: 1, +const destinationWaypoint: Point = new Point({ lat: 48.8566, lon: 2.3522, }); -const intermediateWaypoint: Waypoint = new Waypoint({ - position: 1, +const intermediateWaypoint: Point = new Point({ lat: 48.74488, lon: 4.8972, }); -const destinationWaypointWithIntermediateWaypoint: Waypoint = new Waypoint({ - position: 2, - lat: 48.8566, - lon: 2.3522, -}); describe('Path Creator Service', () => { it('should create a path for a driver only', () => { @@ -58,11 +50,7 @@ describe('Path Creator Service', () => { it('should create two different paths for a driver and passenger with intermediate waypoint', () => { const pathCreator: PathCreator = new PathCreator( [Role.DRIVER, Role.PASSENGER], - [ - originWaypoint, - intermediateWaypoint, - destinationWaypointWithIntermediateWaypoint, - ], + [originWaypoint, intermediateWaypoint, destinationWaypoint], ); const paths: Path[] = pathCreator.getBasePaths(); expect(paths).toHaveLength(2); diff --git a/src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts b/src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts deleted file mode 100644 index da67658..0000000 --- a/src/modules/ad/tests/unit/core/waypoint.value-object.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, -} from '@mobicoop/ddd-library'; -import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object'; - -describe('Waypoint value object', () => { - it('should create a waypoint value object', () => { - const waypointVO = new Waypoint({ - position: 0, - lat: 48.689445, - lon: 6.17651, - }); - expect(waypointVO.position).toBe(0); - expect(waypointVO.lat).toBe(48.689445); - expect(waypointVO.lon).toBe(6.17651); - }); - it('should throw an exception if position is invalid', () => { - try { - new Waypoint({ - position: -1, - lat: 48.689445, - lon: 6.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentInvalidException); - } - }); - it('should throw an exception if longitude is invalid', () => { - try { - new Waypoint({ - position: 0, - lat: 48.689445, - lon: 186.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { - new Waypoint({ - position: 0, - lat: 48.689445, - lon: -186.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - }); - it('should throw an exception if latitude is invalid', () => { - try { - new Waypoint({ - position: 0, - lat: 148.689445, - lon: 6.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { - new Waypoint({ - position: 0, - lat: -148.689445, - lon: 6.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - }); -}); diff --git a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts index 472cdb4..5791bd3 100644 --- a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts @@ -9,7 +9,6 @@ describe('WayStep value object', () => { const wayStepVO = new WayStep({ lat: 48.689445, lon: 6.17651, - position: 0, actors: [ new Actor({ role: Role.DRIVER, @@ -21,7 +20,6 @@ describe('WayStep value object', () => { }), ], }); - expect(wayStepVO.position).toBe(0); expect(wayStepVO.lon).toBe(6.17651); expect(wayStepVO.lat).toBe(48.689445); expect(wayStepVO.actors).toHaveLength(2); @@ -31,7 +29,6 @@ describe('WayStep value object', () => { new WayStep({ lat: 48.689445, lon: 6.17651, - position: 0, actors: [], }); } catch (e: any) { @@ -43,7 +40,6 @@ describe('WayStep value object', () => { new WayStep({ lat: 48.689445, lon: 6.17651, - position: 0, actors: [ new Actor({ role: Role.DRIVER, diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts index 4791b9b..82113c8 100644 --- a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts @@ -61,14 +61,8 @@ describe('Route provider', () => { it('should provide a route', async () => { const route: Route = await routeProvider.getBasic([ - { - position: 0, - ...originPoint, - }, - { - position: 1, - ...destinationPoint, - }, + originPoint, + destinationPoint, ]); expect(route.distance).toBe(350101); expect(route.duration).toBe(14422); diff --git a/src/modules/geography/core/application/ports/georouter.port.ts b/src/modules/geography/core/application/ports/georouter.port.ts index 8936ff0..fa78b58 100644 --- a/src/modules/geography/core/application/ports/georouter.port.ts +++ b/src/modules/geography/core/application/ports/georouter.port.ts @@ -1,6 +1,6 @@ -import { Route, Waypoint } from '../../domain/route.types'; +import { Route, Point } from '../../domain/route.types'; import { GeorouterSettings } from '../types/georouter-settings.type'; export interface GeorouterPort { - route(waypoints: Waypoint[], settings: GeorouterSettings): Promise; + route(waypoints: Point[], settings: GeorouterSettings): Promise; } diff --git a/src/modules/geography/core/application/queries/get-route/get-route.query.ts b/src/modules/geography/core/application/queries/get-route/get-route.query.ts index 6697942..2eecbc0 100644 --- a/src/modules/geography/core/application/queries/get-route/get-route.query.ts +++ b/src/modules/geography/core/application/queries/get-route/get-route.query.ts @@ -1,12 +1,12 @@ import { QueryBase } from '@mobicoop/ddd-library'; import { GeorouterSettings } from '../../types/georouter-settings.type'; -import { Waypoint } from '@modules/geography/core/domain/route.types'; +import { Point } from '@modules/geography/core/domain/route.types'; export class GetRouteQuery extends QueryBase { - readonly waypoints: Waypoint[]; + readonly waypoints: Point[]; readonly georouterSettings: GeorouterSettings; - constructor(waypoints: Waypoint[], georouterSettings: GeorouterSettings) { + constructor(waypoints: Point[], georouterSettings: GeorouterSettings) { super(); this.waypoints = waypoints; this.georouterSettings = georouterSettings; diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index e6ff203..87d9130 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -1,7 +1,6 @@ import { GeorouterPort } from '../application/ports/georouter.port'; import { GeorouterSettings } from '../application/types/georouter-settings.type'; import { PointProps } from './value-objects/point.value-object'; -import { WaypointProps } from './value-objects/waypoint.value-object'; // All properties that a Route has export interface RouteProps { @@ -15,7 +14,7 @@ export interface RouteProps { // Properties that are needed for a Route creation export interface CreateRouteProps { - waypoints: WaypointProps[]; + waypoints: PointProps[]; georouter: GeorouterPort; georouterSettings: GeorouterSettings; } @@ -36,13 +35,7 @@ export type Point = { lat: number; }; -export type Waypoint = Point & { - position: number; -}; - -export type Spacetime = { +export type Step = Point & { duration: number; distance?: number; }; - -export type Step = Point & Spacetime; diff --git a/src/modules/geography/core/domain/value-objects/waypoint.value-object.ts b/src/modules/geography/core/domain/value-objects/waypoint.value-object.ts deleted file mode 100644 index d18fe04..0000000 --- a/src/modules/geography/core/domain/value-objects/waypoint.value-object.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; -import { PointProps } from './point.value-object'; - -/** Note: - * Value Objects with multiple properties can contain - * other Value Objects inside if needed. - * */ - -export interface WaypointProps extends PointProps { - position: number; -} - -export class Waypoint extends ValueObject { - get position(): number { - return this.props.position; - } - - get lon(): number { - return this.props.lon; - } - - get lat(): number { - return this.props.lat; - } - - protected validate(props: WaypointProps): void { - if (props.position < 0) - throw new ArgumentInvalidException( - 'position must be greater than or equal to 0', - ); - } -} diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts index adfc60a..891ec77 100644 --- a/src/modules/geography/infrastructure/graphhopper-georouter.ts +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { GeorouterPort } from '../core/application/ports/georouter.port'; import { GeorouterSettings } from '../core/application/types/georouter-settings.type'; -import { Route, Step, Waypoint } from '../core/domain/route.types'; +import { Route, Step, Point } from '../core/domain/route.types'; import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; import { GEODESIC, PARAMS_PROVIDER } from '../geography.di-tokens'; import { catchError, lastValueFrom, map } from 'rxjs'; @@ -31,7 +31,7 @@ export class GraphhopperGeorouter implements GeorouterPort { } route = async ( - waypoints: Waypoint[], + waypoints: Point[], settings: GeorouterSettings, ): Promise => { this._setDefaultUrlArgs(); @@ -57,12 +57,12 @@ export class GraphhopperGeorouter implements GeorouterPort { } }; - private _getRoute = async (waypoints: Waypoint[]): Promise => { + private _getRoute = async (waypoints: Point[]): Promise => { const url: string = [ this.getUrl(), '&point=', waypoints - .map((waypoint: Waypoint) => [waypoint.lat, waypoint.lon].join('%2C')) + .map((point: Point) => [point.lat, point.lon].join('%2C')) .join('&point='), ].join(''); return await lastValueFrom( diff --git a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts index 1b01076..651df23 100644 --- a/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts +++ b/src/modules/geography/interface/controllers/dtos/get-route.request.dto.ts @@ -1,5 +1,5 @@ -import { Waypoint } from '@modules/geography/core/domain/route.types'; +import { Point } from '@modules/geography/core/domain/route.types'; export type GetRouteRequestDto = { - waypoints: Waypoint[]; + waypoints: Point[]; }; diff --git a/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts b/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts index 8008540..ca8a367 100644 --- a/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts +++ b/src/modules/geography/tests/unit/core/get-route.query-handler.spec.ts @@ -2,17 +2,15 @@ import { GeorouterPort } from '@modules/geography/core/application/ports/georout import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query'; import { GetRouteQueryHandler } from '@modules/geography/core/application/queries/get-route/get-route.query-handler'; import { RouteEntity } from '@modules/geography/core/domain/route.entity'; -import { Waypoint } from '@modules/geography/core/domain/route.types'; +import { Point } from '@modules/geography/core/domain/route.types'; import { GEOROUTER } from '@modules/geography/geography.di-tokens'; import { Test, TestingModule } from '@nestjs/testing'; -const originWaypoint: Waypoint = { - position: 0, +const originWaypoint: Point = { lat: 48.689445, lon: 6.17651, }; -const destinationWaypoint: Waypoint = { - position: 1, +const destinationWaypoint: Point = { lat: 48.8566, lon: 2.3522, }; diff --git a/src/modules/geography/tests/unit/core/route.entity.spec.ts b/src/modules/geography/tests/unit/core/route.entity.spec.ts index 127cf36..93226d5 100644 --- a/src/modules/geography/tests/unit/core/route.entity.spec.ts +++ b/src/modules/geography/tests/unit/core/route.entity.spec.ts @@ -44,16 +44,7 @@ const mockGeorouter: GeorouterPort = { }; const createRouteProps: CreateRouteProps = { - waypoints: [ - { - position: 0, - ...originPoint, - }, - { - position: 1, - ...destinationPoint, - }, - ], + waypoints: [originPoint, destinationPoint], georouter: mockGeorouter, georouterSettings: { points: true, diff --git a/src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts b/src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts deleted file mode 100644 index 3723338..0000000 --- a/src/modules/geography/tests/unit/core/waypoint.value-object.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, -} from '@mobicoop/ddd-library'; -import { Waypoint } from '@modules/geography/core/domain/value-objects/waypoint.value-object'; - -describe('Waypoint value object', () => { - it('should create a waypoint value object', () => { - const waypointVO = new Waypoint({ - position: 0, - lat: 48.689445, - lon: 6.17651, - }); - expect(waypointVO.position).toBe(0); - expect(waypointVO.lat).toBe(48.689445); - expect(waypointVO.lon).toBe(6.17651); - }); - it('should throw an exception if position is invalid', () => { - try { - new Waypoint({ - position: -1, - lat: 48.689445, - lon: 6.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentInvalidException); - } - }); - it('should throw an exception if longitude is invalid', () => { - try { - new Waypoint({ - position: 0, - lat: 48.689445, - lon: 186.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { - new Waypoint({ - position: 0, - lat: 48.689445, - lon: -186.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - }); - it('should throw an exception if latitude is invalid', () => { - try { - new Waypoint({ - position: 0, - lat: 148.689445, - lon: 6.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { - new Waypoint({ - position: 0, - lat: -148.689445, - lon: 6.17651, - }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - }); -}); diff --git a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts index 9686644..419b1bd 100644 --- a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts @@ -297,12 +297,10 @@ describe('Graphhopper Georouter', () => { graphhopperGeorouter.route( [ { - position: 0, lon: 0, lat: 0, }, { - position: 1, lon: 1, lat: 1, }, @@ -321,12 +319,10 @@ describe('Graphhopper Georouter', () => { graphhopperGeorouter.route( [ { - position: 0, lon: 0, lat: 0, }, { - position: 1, lon: 1, lat: 1, }, @@ -344,12 +340,10 @@ describe('Graphhopper Georouter', () => { const route: Route = await graphhopperGeorouter.route( [ { - position: 0, lon: 0, lat: 0, }, { - position: 1, lon: 10, lat: 10, }, @@ -367,12 +361,10 @@ describe('Graphhopper Georouter', () => { const route: Route = await graphhopperGeorouter.route( [ { - position: 0, lon: 0, lat: 0, }, { - position: 1, lon: 10, lat: 10, }, @@ -394,12 +386,10 @@ describe('Graphhopper Georouter', () => { const route: Route = await graphhopperGeorouter.route( [ { - position: 0, lon: 0, lat: 0, }, { - position: 1, lon: 10, lat: 10, }, @@ -419,17 +409,14 @@ describe('Graphhopper Georouter', () => { const route: Route = await graphhopperGeorouter.route( [ { - position: 0, lon: 0, lat: 0, }, { - position: 1, lon: 5, lat: 5, }, { - position: 2, lon: 10, lat: 10, }, @@ -452,12 +439,10 @@ describe('Graphhopper Georouter', () => { const route: Route = await graphhopperGeorouter.route( [ { - position: 0, lon: 0, lat: 0, }, { - position: 1, lon: 10, lat: 10, }, diff --git a/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts b/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts index b4c4202..96484fc 100644 --- a/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts +++ b/src/modules/geography/tests/unit/interface/get-basic-route.controller.spec.ts @@ -49,12 +49,10 @@ describe('Get Basic Route Controller', () => { await getBasicRouteController.get({ waypoints: [ { - position: 0, lat: 48.689445, lon: 6.17651, }, { - position: 1, lat: 48.8566, lon: 2.3522, }, From a7c73080a7b7268929a515e1e8de66986bfe769f Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 14 Sep 2023 17:28:42 +0200 Subject: [PATCH 27/52] improve tests --- .../domain/carpool-path-creator.service.ts | 27 ++++++++++++------- .../value-objects/point.value-object.ts | 3 +++ .../unit/core/point.value-object.spec.ts | 16 +++++++++++ 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index 9af2fd9..aa615a2 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -10,12 +10,11 @@ export class CarpoolPathCreator { private readonly passengerWaypoints: Point[], ) {} - public createCarpoolPath = (): WayStep[] => { - const passengerWaysteps: WayStep[] = this._createPassengerWaysteps(); - if (this.driverWaypoints.length == 2) { - } - return passengerWaysteps; - }; + public createCarpoolPath = (): WayStep[] => + this._createMixedWaysteps( + this._createDriverWaysteps(), + this._createPassengerWaysteps(), + ); private _createDriverWaysteps = (): WayStep[] => this.driverWaypoints.map( @@ -48,7 +47,7 @@ export class CarpoolPathCreator { }); if ( this.driverWaypoints.filter((driverWaypoint: Point) => - this._isSamePoint(driverWaypoint, passengerWaypoint), + passengerWaypoint.isSame(driverWaypoint), ).length > 0 ) { waystep.actors.push( @@ -64,8 +63,18 @@ export class CarpoolPathCreator { return waysteps; }; - private _isSamePoint = (point1: Point, point2: Point): boolean => - point1.lon === point2.lon && point1.lat === point2.lat; + private _createMixedWaysteps = ( + driverWaysteps: WayStep[], + passengerWaysteps: WayStep[], + ): WayStep[] => + driverWaysteps.length == 2 + ? [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]] + : this._createComplexMixedWaysteps(driverWaysteps, passengerWaysteps); + + private _createComplexMixedWaysteps = ( + driverWaysteps: WayStep[], + passengerWaysteps: WayStep[], + ): WayStep[] => []; private _getTarget = (index: number, waypoints: Point[]): Target => index == 0 diff --git a/src/modules/ad/core/domain/value-objects/point.value-object.ts b/src/modules/ad/core/domain/value-objects/point.value-object.ts index 2047ead..54a2677 100644 --- a/src/modules/ad/core/domain/value-objects/point.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/point.value-object.ts @@ -22,6 +22,9 @@ export class Point extends ValueObject { return this.props.lat; } + isSame = (point: this): boolean => + point.lon == this.lon && point.lat == this.lat; + protected validate(props: PointProps): void { if (props.lon > 180 || props.lon < -180) throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); diff --git a/src/modules/ad/tests/unit/core/point.value-object.spec.ts b/src/modules/ad/tests/unit/core/point.value-object.spec.ts index b6980e2..fb9943b 100644 --- a/src/modules/ad/tests/unit/core/point.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/point.value-object.spec.ts @@ -10,6 +10,22 @@ describe('Point value object', () => { expect(pointVO.lat).toBe(48.689445); expect(pointVO.lon).toBe(6.17651); }); + it('should check if two points are identical', () => { + const pointVO = new Point({ + lat: 48.689445, + lon: 6.17651, + }); + const identicalPointVO = new Point({ + lat: 48.689445, + lon: 6.17651, + }); + const differentPointVO = new Point({ + lat: 48.689446, + lon: 6.17651, + }); + expect(pointVO.isSame(identicalPointVO)).toBeTruthy(); + expect(pointVO.isSame(differentPointVO)).toBeFalsy(); + }); it('should throw an exception if longitude is invalid', () => { try { new Point({ From 37fd74d6d3f1f0d96efa202388298b5d0130a146 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 15 Sep 2023 17:02:52 +0200 Subject: [PATCH 28/52] carpool path creator --- ...ssenger-oriented-carpool-path.completer.ts | 13 +- .../domain/carpool-path-creator.service.ts | 184 ++++++++++++++++-- .../value-objects/waystep.value-object.ts | 13 +- .../core/carpool-path-creator.service.spec.ts | 103 ++++++++++ .../unit/core/waystep.value-object.spec.ts | 23 ++- 5 files changed, 291 insertions(+), 45 deletions(-) create mode 100644 src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts index ccba94c..201c5ac 100644 --- a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts @@ -48,18 +48,9 @@ export class PassengerOrientedCarpoolPathCompleter extends Completer { }), ), ); - candidate.setCarpoolPath(carpoolPathCreator.createCarpoolPath()); - console.log(JSON.stringify(candidate, null, 2)); + candidate.setCarpoolPath(carpoolPathCreator.carpoolPath()); + // console.log(JSON.stringify(candidate, null, 2)); }); return candidates; }; } - -// complete = async (candidates: Candidate[]): Promise => { -// candidates.forEach( (candidate: Candidate) => { -// if (candidate.role == Role.DRIVER) { -// candidate.driverWaypoints = th -// } - -// return candidates; -// } diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index aa615a2..252a503 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -5,23 +5,41 @@ import { Point } from './value-objects/point.value-object'; import { WayStep } from './value-objects/waystep.value-object'; export class CarpoolPathCreator { + private PRECISION = 5; + constructor( private readonly driverWaypoints: Point[], private readonly passengerWaypoints: Point[], ) {} - public createCarpoolPath = (): WayStep[] => - this._createMixedWaysteps( - this._createDriverWaysteps(), - this._createPassengerWaysteps(), + /** + * Creates a path (a list of waysteps) between driver waypoints + and passenger waypoints respecting the order + of the driver waypoints + Inspired by : + https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + */ + public carpoolPath = (): WayStep[] => + this._consolidate( + this._mixedWaysteps(this._driverWaysteps(), this._passengerWaysteps()), ); - private _createDriverWaysteps = (): WayStep[] => + private _mixedWaysteps = ( + driverWaysteps: WayStep[], + passengerWaysteps: WayStep[], + ): WayStep[] => + driverWaysteps.length == 2 + ? this._simpleMixedWaysteps(driverWaysteps, passengerWaysteps) + : this._complexMixedWaysteps(driverWaysteps, passengerWaysteps); + + private _driverWaysteps = (): WayStep[] => this.driverWaypoints.map( (waypoint: Point, index: number) => new WayStep({ - lon: waypoint.lon, - lat: waypoint.lat, + point: new Point({ + lon: waypoint.lon, + lat: waypoint.lat, + }), actors: [ new Actor({ role: Role.DRIVER, @@ -31,13 +49,18 @@ export class CarpoolPathCreator { }), ); - private _createPassengerWaysteps = (): WayStep[] => { + /** + * Creates the passenger waysteps with original passenger waypoints, adding driver waypoints that are the same + */ + private _passengerWaysteps = (): WayStep[] => { const waysteps: WayStep[] = []; this.passengerWaypoints.forEach( (passengerWaypoint: Point, index: number) => { const waystep: WayStep = new WayStep({ - lon: passengerWaypoint.lon, - lat: passengerWaypoint.lat, + point: new Point({ + lon: passengerWaypoint.lon, + lat: passengerWaypoint.lat, + }), actors: [ new Actor({ role: Role.PASSENGER, @@ -48,7 +71,7 @@ export class CarpoolPathCreator { if ( this.driverWaypoints.filter((driverWaypoint: Point) => passengerWaypoint.isSame(driverWaypoint), - ).length > 0 + ).length == 0 ) { waystep.actors.push( new Actor({ @@ -63,18 +86,114 @@ export class CarpoolPathCreator { return waysteps; }; - private _createMixedWaysteps = ( + private _simpleMixedWaysteps = ( driverWaysteps: WayStep[], passengerWaysteps: WayStep[], - ): WayStep[] => - driverWaysteps.length == 2 - ? [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]] - : this._createComplexMixedWaysteps(driverWaysteps, passengerWaysteps); + ): WayStep[] => [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]]; - private _createComplexMixedWaysteps = ( + private _complexMixedWaysteps = ( driverWaysteps: WayStep[], passengerWaysteps: WayStep[], - ): WayStep[] => []; + ): WayStep[] => { + let mixedWaysteps: WayStep[] = [...driverWaysteps]; + const originInsertIndex: number = this._insertIndex( + passengerWaysteps[0], + driverWaysteps, + ); + mixedWaysteps = [ + ...mixedWaysteps.slice(0, originInsertIndex), + passengerWaysteps[0], + ...mixedWaysteps.slice(originInsertIndex), + ]; + const destinationInsertIndex: number = + this._insertIndex( + passengerWaysteps[passengerWaysteps.length - 1], + driverWaysteps, + ) + 1; + mixedWaysteps = [ + ...mixedWaysteps.slice(0, destinationInsertIndex), + passengerWaysteps[passengerWaysteps.length - 1], + ...mixedWaysteps.slice(destinationInsertIndex), + ]; + return mixedWaysteps; + }; + + private _insertIndex = ( + targetWaystep: WayStep, + waysteps: WayStep[], + ): number => + this._closestSegmentIndex(targetWaystep, this._segments(waysteps)) + 1; + + private _segments = (waysteps: WayStep[]): WayStep[][] => { + const segments: WayStep[][] = []; + waysteps.forEach((waystep: WayStep, index: number) => { + if (index < waysteps.length - 1) + segments.push([waystep, waysteps[index + 1]]); + }); + return segments; + }; + + private _closestSegmentIndex = ( + waystep: WayStep, + segments: WayStep[][], + ): number => { + const distances: Map = new Map(); + segments.forEach((segment: WayStep[], index: number) => { + distances.set(index, this._distanceToSegment(waystep, segment)); + }); + const sortedDistances: Map = new Map( + [...distances.entries()].sort((a, b) => a[1] - b[1]), + ); + const [closestSegmentIndex] = sortedDistances.keys(); + return closestSegmentIndex; + }; + + private _distanceToSegment = (waystep: WayStep, segment: WayStep[]): number => + parseFloat( + Math.sqrt(this._distanceToSegmentSquared(waystep, segment)).toFixed( + this.PRECISION, + ), + ); + + private _distanceToSegmentSquared = ( + waystep: WayStep, + segment: WayStep[], + ): number => { + const length2: number = this._distanceSquared( + segment[0].point, + segment[1].point, + ); + if (length2 == 0) + return this._distanceSquared(waystep.point, segment[0].point); + const length: number = Math.max( + 0, + Math.min( + 1, + ((waystep.point.lon - segment[0].point.lon) * + (segment[1].point.lon - segment[0].point.lon) + + (waystep.point.lat - segment[0].point.lat) * + (segment[1].point.lat - segment[0].point.lat)) / + length2, + ), + ); + const newPoint: Point = new Point({ + lon: + segment[0].point.lon + + length * (segment[1].point.lon - segment[0].point.lon), + lat: + segment[0].point.lat + + length * (segment[1].point.lat - segment[0].point.lat), + }); + return this._distanceSquared(waystep.point, newPoint); + }; + + private _distanceSquared = (point1: Point, point2: Point): number => + parseFloat( + ( + Math.pow(point1.lon - point2.lon, 2) + + Math.pow(point1.lat - point2.lat, 2) + ).toFixed(this.PRECISION), + ); private _getTarget = (index: number, waypoints: Point[]): Target => index == 0 @@ -82,4 +201,33 @@ export class CarpoolPathCreator { : index == waypoints.length - 1 ? Target.FINISH : Target.INTERMEDIATE; + + /** + * Consolidate waysteps by removing duplicate actors (eg. driver with neutral and start or finish target) + */ + private _consolidate = (waysteps: WayStep[]): WayStep[] => { + const uniquePoints: Point[] = []; + waysteps.forEach((waystep: WayStep) => { + if ( + uniquePoints.find((point: Point) => point.isSame(waystep.point)) === + undefined + ) + uniquePoints.push( + new Point({ + lon: waystep.point.lon, + lat: waystep.point.lat, + }), + ); + }); + return uniquePoints.map( + (point: Point) => + new WayStep({ + point, + actors: waysteps + .filter((waystep: WayStep) => waystep.point.isSame(point)) + .map((waystep: WayStep) => waystep.actors) + .flat(), + }), + ); + }; } diff --git a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts index 7797525..bfc1f52 100644 --- a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/waystep.value-object.ts @@ -4,24 +4,21 @@ import { } from '@mobicoop/ddd-library'; import { Actor } from './actor.value-object'; import { Role } from '../ad.types'; -import { PointProps } from './point.value-object'; +import { Point } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface WayStepProps extends PointProps { +export interface WayStepProps { + point: Point; actors: Actor[]; } export class WayStep extends ValueObject { - get lon(): number { - return this.props.lon; - } - - get lat(): number { - return this.props.lat; + get point(): Point { + return this.props.point; } get actors(): Actor[] { diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts new file mode 100644 index 0000000..4379d95 --- /dev/null +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -0,0 +1,103 @@ +import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; +import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; + +const waypoint1: Point = new Point({ + lat: 0, + lon: 0, +}); +const waypoint2: Point = new Point({ + lat: 2, + lon: 2, +}); +const waypoint3: Point = new Point({ + lat: 5, + lon: 5, +}); +const waypoint4: Point = new Point({ + lat: 6, + lon: 6, +}); +const waypoint5: Point = new Point({ + lat: 8, + lon: 8, +}); +const waypoint6: Point = new Point({ + lat: 10, + lon: 10, +}); + +describe('Carpool Path Creator Service', () => { + it('should create a simple carpool path', () => { + const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( + [waypoint1, waypoint6], + [waypoint2, waypoint5], + ); + const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); + expect(waysteps).toHaveLength(4); + expect(waysteps[0].actors.length).toBe(1); + }); + it('should create a simple carpool path with same destination for driver and passenger', () => { + const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( + [waypoint1, waypoint6], + [waypoint2, waypoint6], + ); + const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); + expect(waysteps).toHaveLength(3); + expect(waysteps[0].actors.length).toBe(1); + expect(waysteps[1].actors.length).toBe(2); + expect(waysteps[2].actors.length).toBe(2); + }); + it('should create a simple carpool path with same waypoints for driver and passenger', () => { + const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( + [waypoint1, waypoint6], + [waypoint1, waypoint6], + ); + const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); + expect(waysteps).toHaveLength(2); + expect(waysteps[0].actors.length).toBe(2); + expect(waysteps[1].actors.length).toBe(2); + }); + it('should create a complex carpool path with 3 driver waypoints', () => { + const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( + [waypoint1, waypoint3, waypoint6], + [waypoint2, waypoint5], + ); + const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); + expect(waysteps).toHaveLength(5); + expect(waysteps[0].actors.length).toBe(1); + expect(waysteps[1].actors.length).toBe(2); + expect(waysteps[2].actors.length).toBe(1); + expect(waysteps[3].actors.length).toBe(2); + expect(waysteps[4].actors.length).toBe(1); + }); + it('should create a complex carpool path with 4 driver waypoints', () => { + const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( + [waypoint1, waypoint3, waypoint4, waypoint6], + [waypoint2, waypoint5], + ); + const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); + expect(waysteps).toHaveLength(6); + expect(waysteps[0].actors.length).toBe(1); + expect(waysteps[1].actors.length).toBe(2); + expect(waysteps[2].actors.length).toBe(1); + expect(waysteps[3].actors.length).toBe(1); + expect(waysteps[4].actors.length).toBe(2); + expect(waysteps[5].actors.length).toBe(1); + }); + it('should create a alternate complex carpool path with 4 driver waypoints', () => { + const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( + [waypoint1, waypoint2, waypoint5, waypoint6], + [waypoint3, waypoint4], + ); + const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); + // console.log(JSON.stringify(waysteps, null, 2)); + expect(waysteps).toHaveLength(6); + expect(waysteps[0].actors.length).toBe(1); + expect(waysteps[1].actors.length).toBe(1); + expect(waysteps[2].actors.length).toBe(2); + expect(waysteps[3].actors.length).toBe(2); + expect(waysteps[4].actors.length).toBe(1); + expect(waysteps[5].actors.length).toBe(1); + }); +}); diff --git a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts index 5791bd3..c71f530 100644 --- a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts @@ -2,13 +2,16 @@ import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; describe('WayStep value object', () => { it('should create a waystep value object', () => { const wayStepVO = new WayStep({ - lat: 48.689445, - lon: 6.17651, + point: new Point({ + lat: 48.689445, + lon: 6.17651, + }), actors: [ new Actor({ role: Role.DRIVER, @@ -20,15 +23,17 @@ describe('WayStep value object', () => { }), ], }); - expect(wayStepVO.lon).toBe(6.17651); - expect(wayStepVO.lat).toBe(48.689445); + expect(wayStepVO.point.lon).toBe(6.17651); + expect(wayStepVO.point.lat).toBe(48.689445); expect(wayStepVO.actors).toHaveLength(2); }); it('should throw an exception if actors is empty', () => { try { new WayStep({ - lat: 48.689445, - lon: 6.17651, + point: new Point({ + lat: 48.689445, + lon: 6.17651, + }), actors: [], }); } catch (e: any) { @@ -38,8 +43,10 @@ describe('WayStep value object', () => { it('should throw an exception if actors contains more than one driver', () => { try { new WayStep({ - lat: 48.689445, - lon: 6.17651, + point: new Point({ + lat: 48.689445, + lon: 6.17651, + }), actors: [ new Actor({ role: Role.DRIVER, From 40227be69ab7d897b582ae051ce6fe5f372b4960 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 15 Sep 2023 17:03:28 +0200 Subject: [PATCH 29/52] carpool path creator --- .../ad/tests/unit/core/carpool-path-creator.service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index 4379d95..6d090e1 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -91,7 +91,6 @@ describe('Carpool Path Creator Service', () => { [waypoint3, waypoint4], ); const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - // console.log(JSON.stringify(waysteps, null, 2)); expect(waysteps).toHaveLength(6); expect(waysteps[0].actors.length).toBe(1); expect(waysteps[1].actors.length).toBe(1); From a277a9547f762f8edba11627cc147a78bdbcd3b1 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 18 Sep 2023 11:14:46 +0200 Subject: [PATCH 30/52] fail faster in path creator --- .../commands/create-ad/create-ad.service.ts | 107 +++++++++--------- .../domain/carpool-path-creator.service.ts | 12 +- src/modules/ad/core/domain/match.errors.ts | 21 ++++ .../ad/core/domain/path-creator.service.ts | 21 +++- .../core/carpool-path-creator.service.spec.ts | 15 +++ .../tests/unit/core/create-ad.service.spec.ts | 13 ++- .../core/passenger-oriented-algorithm.spec.ts | 11 +- .../unit/core/path-creator.service.spec.ts | 18 +++ 8 files changed, 162 insertions(+), 56 deletions(-) create mode 100644 src/modules/ad/core/domain/match.errors.ts diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 1f7c55e..92c2e72 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -31,6 +31,7 @@ export class CreateAdService implements ICommandHandler { const roles: Role[] = []; if (command.driver) roles.push(Role.DRIVER); if (command.passenger) roles.push(Role.PASSENGER); + const pathCreator: PathCreator = new PathCreator( roles, command.waypoints.map( @@ -41,6 +42,7 @@ export class CreateAdService implements ICommandHandler { }), ), ); + let typedRoutes: TypedRoute[]; try { typedRoutes = await Promise.all( @@ -60,24 +62,11 @@ export class CreateAdService implements ICommandHandler { let points: PointValueObject[] | undefined; let fwdAzimuth: number | undefined; let backAzimuth: number | undefined; - typedRoutes.forEach((typedRoute: TypedRoute) => { - if (typedRoute.type !== PathType.PASSENGER) { - driverDistance = typedRoute.route.distance; - driverDuration = typedRoute.route.duration; - points = typedRoute.route.points.map( - (point: Point) => - new PointValueObject({ - lon: point.lon, - lat: point.lat, - }), - ); - fwdAzimuth = typedRoute.route.fwdAzimuth; - backAzimuth = typedRoute.route.backAzimuth; - } - if (typedRoute.type !== PathType.DRIVER) { - passengerDistance = typedRoute.route.distance; - passengerDuration = typedRoute.route.duration; - if (!points) + try { + typedRoutes.forEach((typedRoute: TypedRoute) => { + if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) { + driverDistance = typedRoute.route.distance; + driverDuration = typedRoute.route.duration; points = typedRoute.route.points.map( (point: Point) => new PointValueObject({ @@ -85,41 +74,55 @@ export class CreateAdService implements ICommandHandler { lat: point.lat, }), ); - if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; - if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; - } - }); - if (points && fwdAzimuth && backAzimuth) { - const ad = AdEntity.create({ - id: command.id, - driver: command.driver, - passenger: command.passenger, - frequency: command.frequency, - fromDate: command.fromDate, - toDate: command.toDate, - schedule: command.schedule, - seatsProposed: command.seatsProposed, - seatsRequested: command.seatsRequested, - strict: command.strict, - waypoints: command.waypoints, - points, - driverDistance, - driverDuration, - passengerDistance, - passengerDuration, - fwdAzimuth, - backAzimuth, - }); - try { - await this.repository.insertExtra(ad, 'ad'); - return ad.id; - } catch (error: any) { - if (error instanceof ConflictException) { - throw new AdAlreadyExistsException(error); + fwdAzimuth = typedRoute.route.fwdAzimuth; + backAzimuth = typedRoute.route.backAzimuth; } - throw error; - } + if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) { + passengerDistance = typedRoute.route.distance; + passengerDuration = typedRoute.route.duration; + if (!points) + points = typedRoute.route.points.map( + (point: Point) => + new PointValueObject({ + lon: point.lon, + lat: point.lat, + }), + ); + if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; + if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; + } + }); + } catch (error: any) { + throw new Error('Invalid route'); + } + const ad = AdEntity.create({ + id: command.id, + driver: command.driver, + passenger: command.passenger, + frequency: command.frequency, + fromDate: command.fromDate, + toDate: command.toDate, + schedule: command.schedule, + seatsProposed: command.seatsProposed, + seatsRequested: command.seatsRequested, + strict: command.strict, + waypoints: command.waypoints, + points: points as PointValueObject[], + driverDistance, + driverDuration, + passengerDistance, + passengerDuration, + fwdAzimuth: fwdAzimuth as number, + backAzimuth: backAzimuth as number, + }); + try { + await this.repository.insertExtra(ad, 'ad'); + return ad.id; + } catch (error: any) { + if (error instanceof ConflictException) { + throw new AdAlreadyExistsException(error); + } + throw error; } - throw new Error('Route error'); } } diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index 252a503..bf1f5ae 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -1,5 +1,6 @@ import { Role } from './ad.types'; import { Target } from './candidate.types'; +import { CarpoolPathCreatorException } from './match.errors'; import { Actor } from './value-objects/actor.value-object'; import { Point } from './value-objects/point.value-object'; import { WayStep } from './value-objects/waystep.value-object'; @@ -10,7 +11,16 @@ export class CarpoolPathCreator { constructor( private readonly driverWaypoints: Point[], private readonly passengerWaypoints: Point[], - ) {} + ) { + if (driverWaypoints.length < 2) + throw new CarpoolPathCreatorException( + new Error('At least 2 driver waypoints must be defined'), + ); + if (passengerWaypoints.length < 2) + throw new CarpoolPathCreatorException( + new Error('At least 2 passenger waypoints must be defined'), + ); + } /** * Creates a path (a list of waysteps) between driver waypoints diff --git a/src/modules/ad/core/domain/match.errors.ts b/src/modules/ad/core/domain/match.errors.ts new file mode 100644 index 0000000..91484bf --- /dev/null +++ b/src/modules/ad/core/domain/match.errors.ts @@ -0,0 +1,21 @@ +import { ExceptionBase } from '@mobicoop/ddd-library'; + +export class PathCreatorException extends ExceptionBase { + static readonly message = 'Path creator error'; + + public readonly code = 'MATCHER.PATH_CREATOR'; + + constructor(cause?: Error, metadata?: unknown) { + super(PathCreatorException.message, cause, metadata); + } +} + +export class CarpoolPathCreatorException extends ExceptionBase { + static readonly message = 'Carpool path creator error'; + + public readonly code = 'MATCHER.CARPOOL_PATH_CREATOR'; + + constructor(cause?: Error, metadata?: unknown) { + super(CarpoolPathCreatorException.message, cause, metadata); + } +} diff --git a/src/modules/ad/core/domain/path-creator.service.ts b/src/modules/ad/core/domain/path-creator.service.ts index f44d147..36114b6 100644 --- a/src/modules/ad/core/domain/path-creator.service.ts +++ b/src/modules/ad/core/domain/path-creator.service.ts @@ -1,12 +1,22 @@ import { Route } from '@modules/geography/core/domain/route.types'; import { Role } from './ad.types'; import { Point } from './value-objects/point.value-object'; +import { PathCreatorException } from './match.errors'; export class PathCreator { constructor( private readonly roles: Role[], private readonly waypoints: Point[], - ) {} + ) { + if (roles.length == 0) + throw new PathCreatorException( + new Error('At least a role must be defined'), + ); + if (waypoints.length < 2) + throw new PathCreatorException( + new Error('At least 2 waypoints must be defined'), + ); + } public getBasePaths = (): Path[] => { const paths: Path[] = []; @@ -61,6 +71,15 @@ export type TypedRoute = { route: Route; }; +/** + * PathType id used for route calculation, to reduce the number of routes to compute : + * - a single route for a driver only + * - a single route for a passenger only + * - a single route for a driver and passenger with 2 waypoints given + * - two routes for a driver and passenger with more than 2 waypoints given + * (all the waypoints as driver, only origin and destination as passenger as + * intermediate waypoints doesn't matter in that case) + */ export enum PathType { GENERIC = 'generic', DRIVER = 'driver', diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index 6d090e1..0c49df7 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -1,4 +1,5 @@ import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; +import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; @@ -99,4 +100,18 @@ describe('Carpool Path Creator Service', () => { expect(waysteps[4].actors.length).toBe(1); expect(waysteps[5].actors.length).toBe(1); }); + it('should throw an exception if less than 2 driver waypoints are given', () => { + try { + new CarpoolPathCreator([waypoint1], [waypoint3, waypoint4]); + } catch (e: any) { + expect(e).toBeInstanceOf(CarpoolPathCreatorException); + } + }); + it('should throw an exception if less than 2 passenger waypoints are given', () => { + try { + new CarpoolPathCreator([waypoint1, waypoint6], [waypoint3]); + } catch (e: any) { + expect(e).toBeInstanceOf(CarpoolPathCreatorException); + } + }); }); diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 4ec978c..4198e68 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -49,6 +49,7 @@ const mockAdRepository = { insertExtra: jest .fn() .mockImplementationOnce(() => ({})) + .mockImplementationOnce(() => ({})) .mockImplementationOnce(() => { throw new Error(); }) @@ -131,7 +132,7 @@ describe('create-ad.service', () => { createAdService.execute(createAdCommand), ).rejects.toBeInstanceOf(Error); }); - it('should create a new ad', async () => { + it('should create a new ad as driver and passenger', async () => { AdEntity.create = jest.fn().mockReturnValue({ id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', }); @@ -140,6 +141,16 @@ describe('create-ad.service', () => { ); expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da'); }); + it('should create a new ad as passenger', async () => { + AdEntity.create = jest.fn().mockReturnValue({ + id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', + }); + const result: AggregateID = await createAdService.execute({ + ...createAdCommand, + driver: false, + }); + expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da'); + }); it('should throw an error if something bad happens', async () => { await expect( createAdService.execute(createAdCommand), diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index 66a1bb4..8c72f43 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -55,7 +55,16 @@ const mockMatcherRepository: AdRepositoryPort = { { id: 'cc260669-1c6d-441f-80a5-19cd59afb777', getProps: jest.fn().mockImplementation(() => ({ - waypoints: [], + waypoints: [ + { + lat: 48.6645, + lon: 6.18457, + }, + { + lat: 48.7898, + lon: 2.36845, + }, + ], })), }, ]), diff --git a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts index dfb85f1..7c7c5d0 100644 --- a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts @@ -1,4 +1,5 @@ import { Role } from '@modules/ad/core/domain/ad.types'; +import { PathCreatorException } from '@modules/ad/core/domain/match.errors'; import { Path, PathCreator, @@ -68,4 +69,21 @@ describe('Path Creator Service', () => { .waypoints, ).toHaveLength(2); }); + it('should throw an exception if a role is not given', () => { + try { + new PathCreator( + [], + [originWaypoint, intermediateWaypoint, destinationWaypoint], + ); + } catch (e: any) { + expect(e).toBeInstanceOf(PathCreatorException); + } + }); + it('should throw an exception if less than 2 waypoints are given', () => { + try { + new PathCreator([Role.DRIVER], [originWaypoint]); + } catch (e: any) { + expect(e).toBeInstanceOf(PathCreatorException); + } + }); }); From 067854b697e9774de1e2c4c60b31c6b603907d3c Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 18 Sep 2023 14:09:33 +0200 Subject: [PATCH 31/52] basic RouteCompleter --- .../queries/match/algorithm.abstract.ts | 6 +- ...ssenger-oriented-carpool-path.completer.ts | 1 - .../match/completer/route.completer.ts | 34 +++ .../queries/match/match.query-handler.ts | 6 +- .../application/queries/match/match.query.ts | 8 +- .../match/passenger-oriented-algorithm.ts | 5 + .../ad/core/domain/candidate.entity.ts | 5 + src/modules/ad/core/domain/candidate.types.ts | 7 +- .../grpc-controllers/match.grpc-controller.ts | 12 +- .../unit/core/match.query-handler.spec.ts | 42 ++-- .../ad/tests/unit/core/match.query.spec.ts | 219 ++++++++++-------- .../core/passenger-oriented-algorithm.spec.ts | 38 +-- ...er-oriented-carpool-path-completer.spec.ts | 35 +-- .../passenger-oriented-geo-filter.spec.ts | 35 +-- .../core/passenger-oriented-selector.spec.ts | 47 ++-- .../interface/match.grpc.controller.spec.ts | 10 + 16 files changed, 301 insertions(+), 209 deletions(-) create mode 100644 src/modules/ad/core/application/queries/match/completer/route.completer.ts diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index f993730..faeb9bc 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -20,6 +20,7 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } + // console.log(JSON.stringify(this.candidates, null, 2)); return this.candidates.map((candidate: CandidateEntity) => MatchEntity.create({ adId: candidate.id }), ); @@ -43,9 +44,6 @@ export abstract class Selector { * A processor processes candidates information */ export abstract class Processor { - protected readonly query: MatchQuery; - constructor(query: MatchQuery) { - this.query = query; - } + constructor(protected readonly query: MatchQuery) {} abstract execute(candidates: CandidateEntity[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts index 201c5ac..f5232ab 100644 --- a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-carpool-path.completer.ts @@ -49,7 +49,6 @@ export class PassengerOrientedCarpoolPathCompleter extends Completer { ), ); candidate.setCarpoolPath(carpoolPathCreator.carpoolPath()); - // console.log(JSON.stringify(candidate, null, 2)); }); return candidates; }; diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts new file mode 100644 index 0000000..2f516ec --- /dev/null +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -0,0 +1,34 @@ +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { Completer } from './completer.abstract'; +import { MatchQuery } from '../match.query'; +import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; + +export class RouteCompleter extends Completer { + protected readonly type: RouteCompleterType; + constructor(query: MatchQuery, type: RouteCompleterType) { + super(query); + this.type = type; + } + + complete = async ( + candidates: CandidateEntity[], + ): Promise => { + await Promise.all( + candidates.map(async (candidate: CandidateEntity) => { + const candidateRoute = await this.query.routeProvider.getBasic( + (candidate.getProps().carpoolSteps as WayStep[]).map( + (wayStep: WayStep) => wayStep.point, + ), + ); + candidate.setMetrics(candidateRoute.distance, candidateRoute.duration); + return candidate; + }), + ); + return candidates; + }; +} + +export enum RouteCompleterType { + BASIC = 'basic', + DETAILED = 'detailed', +} diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 14a8af5..fb263fc 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -7,7 +7,6 @@ import { Inject } from '@nestjs/common'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; import { AD_REPOSITORY, - AD_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; @@ -15,7 +14,6 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port'; import { DefaultParams } from '../../ports/default-params.type'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; -import { RouteProviderPort } from '../../ports/route-provider.port'; @QueryHandler(MatchQuery) export class MatchQueryHandler implements IQueryHandler { @@ -27,8 +25,6 @@ export class MatchQueryHandler implements IQueryHandler { @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, - @Inject(AD_ROUTE_PROVIDER) - private readonly routeProvider: RouteProviderPort, ) { this._defaultParams = defaultParamsProvider.getParams(); } @@ -54,7 +50,7 @@ export class MatchQueryHandler implements IQueryHandler { maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, }) .setDatesAndSchedule(this.datetimeTransformer); - await query.setRoutes(this.routeProvider); + await query.setRoutes(); let algorithm: Algorithm; switch (query.algorithmType) { diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 3fb9a4d..8f96427 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -39,8 +39,9 @@ export class MatchQuery extends QueryBase { passengerRoute?: Route; backAzimuth?: number; private readonly originWaypoint: Waypoint; + routeProvider: RouteProviderPort; - constructor(props: MatchRequestDto) { + constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) { super(); this.driver = props.driver; this.passenger = props.passenger; @@ -65,6 +66,7 @@ export class MatchQuery extends QueryBase { this.originWaypoint = this.waypoints.filter( (waypoint: Waypoint) => waypoint.position == 0, )[0]; + this.routeProvider = routeProvider; } setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => { @@ -178,7 +180,7 @@ export class MatchQuery extends QueryBase { return this; }; - setRoutes = async (routeProvider: RouteProviderPort): Promise => { + setRoutes = async (): Promise => { const roles: Role[] = []; if (this.driver) roles.push(Role.DRIVER); if (this.passenger) roles.push(Role.PASSENGER); @@ -197,7 +199,7 @@ export class MatchQuery extends QueryBase { await Promise.all( pathCreator.getBasePaths().map(async (path: Path) => ({ type: path.type, - route: await routeProvider.getBasic(path.waypoints), + route: await this.routeProvider.getBasic(path.waypoints), })), ) ).forEach((typedRoute: TypedRoute) => { diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index dc0642b..d9bc5af 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -4,6 +4,10 @@ import { PassengerOrientedCarpoolPathCompleter } from './completer/passenger-ori import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter'; import { AdRepositoryPort } from '../../ports/ad.repository.port'; import { PassengerOrientedSelector } from './selector/passenger-oriented.selector'; +import { + RouteCompleter, + RouteCompleterType, +} from './completer/route.completer'; export class PassengerOrientedAlgorithm extends Algorithm { constructor( @@ -14,6 +18,7 @@ export class PassengerOrientedAlgorithm extends Algorithm { this.selector = new PassengerOrientedSelector(query, repository); this.processors = [ new PassengerOrientedCarpoolPathCompleter(query), + new RouteCompleter(query, RouteCompleterType.BASIC), new PassengerOrientedGeoFilter(query), ]; } diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 3add4e3..d932eb9 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -14,6 +14,11 @@ export class CandidateEntity extends AggregateRoot { this.props.carpoolSteps = waySteps; }; + setMetrics = (distance: number, duration: number): void => { + this.props.distance = distance; + this.props.duration = duration; + }; + validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index b86b09b..84af6ef 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -10,6 +10,8 @@ export interface CandidateProps { driverDistance: number; driverDuration: number; carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) + distance?: number; + duration?: number; } // Properties that are needed for a Candidate creation @@ -22,11 +24,6 @@ export interface CreateCandidateProps { passengerWaypoints: PointProps[]; } -export type Spacetime = { - duration: number; - distance?: number; -}; - export enum Target { START = 'START', INTERMEDIATE = 'INTERMEDIATE', diff --git a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index 826b5f5..1f80b69 100644 --- a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -1,4 +1,4 @@ -import { Controller, UsePipes } from '@nestjs/common'; +import { Controller, Inject, UsePipes } from '@nestjs/common'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { ResponseBase, RpcValidationPipe } from '@mobicoop/ddd-library'; import { RpcExceptionCode } from '@mobicoop/ddd-library'; @@ -7,6 +7,8 @@ import { QueryBus } from '@nestjs/cqrs'; import { MatchRequestDto } from './dtos/match.request.dto'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; @UsePipes( new RpcValidationPipe({ @@ -16,13 +18,17 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity'; ) @Controller() export class MatchGrpcController { - constructor(private readonly queryBus: QueryBus) {} + constructor( + private readonly queryBus: QueryBus, + @Inject(AD_ROUTE_PROVIDER) + private readonly routeProvider: RouteProviderPort, + ) {} @GrpcMethod('MatcherService', 'Match') async match(data: MatchRequestDto): Promise { try { const matches: MatchEntity[] = await this.queryBus.execute( - new MatchQuery(data), + new MatchQuery(data, this.routeProvider), ); return new MatchPaginatedResponseDto({ data: matches.map((match: MatchEntity) => ({ diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index d02303c..c96bd3b 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,6 +1,5 @@ import { AD_REPOSITORY, - AD_ROUTE_PROVIDER, INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; @@ -114,10 +113,6 @@ describe('Match Query Handler', () => { provide: INPUT_DATETIME_TRANSFORMER, useValue: mockInputDateTimeTransformer, }, - { - provide: AD_ROUTE_PROVIDER, - useValue: mockRouteProvider, - }, ], }).compile(); @@ -129,23 +124,26 @@ describe('Match Query Handler', () => { }); it('should return a Match entity', async () => { - const matchQuery = new MatchQuery({ - algorithmType: AlgorithmType.PASSENGER_ORIENTED, - driver: false, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '07:05', - day: 1, - margin: 900, - }, - ], - strict: false, - waypoints: [originWaypoint, destinationWaypoint], - }); + const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + day: 1, + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); const matches: MatchEntity[] = await matchQueryHandler.execute(matchQuery); expect(matches.length).toBeGreaterThanOrEqual(0); }); diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts index 17440de..8c0c580 100644 --- a/src/modules/ad/tests/unit/core/match.query.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -108,17 +108,20 @@ const mockRouteProvider: RouteProviderPort = { describe('Match Query', () => { it('should set default values', async () => { - const matchQuery = new MatchQuery({ - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '01:05', - }, - ], - waypoints: [originWaypoint, destinationWaypoint], - }); + const matchQuery = new MatchQuery( + { + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); matchQuery .setMissingMarginDurations(defaultParams.DEPARTURE_TIME_MARGIN) .setMissingStrict(defaultParams.STRICT) @@ -159,19 +162,22 @@ describe('Match Query', () => { }); it('should set good values for seats', async () => { - const matchQuery = new MatchQuery({ - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - seatsProposed: -1, - seatsRequested: -1, - schedule: [ - { - time: '07:05', - }, - ], - waypoints: [originWaypoint, destinationWaypoint], - }); + const matchQuery = new MatchQuery( + { + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + seatsProposed: -1, + seatsRequested: -1, + schedule: [ + { + time: '07:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); matchQuery.setDefaultDriverAndPassengerParameters({ driver: defaultParams.DRIVER, passenger: defaultParams.PASSENGER, @@ -183,101 +189,114 @@ describe('Match Query', () => { }); it('should set route for a driver only', async () => { - const matchQuery = new MatchQuery({ - driver: true, - passenger: false, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '01:05', - }, - ], - waypoints: [originWaypoint, destinationWaypoint], - }); - await matchQuery.setRoutes(mockRouteProvider); + const matchQuery = new MatchQuery( + { + driver: true, + passenger: false, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + await matchQuery.setRoutes(); expect(matchQuery.driverRoute?.distance).toBe(350101); expect(matchQuery.passengerRoute).toBeUndefined(); }); it('should set route for a passenger only', async () => { - const matchQuery = new MatchQuery({ - driver: false, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '01:05', - }, - ], - waypoints: [originWaypoint, destinationWaypoint], - }); - await matchQuery.setRoutes(mockRouteProvider); + const matchQuery = new MatchQuery( + { + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + await matchQuery.setRoutes(); expect(matchQuery.passengerRoute?.distance).toBe(340102); expect(matchQuery.driverRoute).toBeUndefined(); }); it('should set route for a driver and passenger', async () => { - const matchQuery = new MatchQuery({ - driver: true, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '01:05', - }, - ], - waypoints: [originWaypoint, destinationWaypoint], - }); - await matchQuery.setRoutes(mockRouteProvider); + const matchQuery = new MatchQuery( + { + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + await matchQuery.setRoutes(); expect(matchQuery.driverRoute?.distance).toBe(350101); expect(matchQuery.passengerRoute?.distance).toBe(350101); }); it('should set route for a driver and passenger with 3 waypoints', async () => { - const matchQuery = new MatchQuery({ - driver: true, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '01:05', - }, - ], - waypoints: [ - originWaypoint, - intermediateWaypoint, - { ...destinationWaypoint, position: 2 }, - ], - }); - await matchQuery.setRoutes(mockRouteProvider); + const matchQuery = new MatchQuery( + { + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [ + originWaypoint, + intermediateWaypoint, + { ...destinationWaypoint, position: 2 }, + ], + }, + mockRouteProvider, + ); + await matchQuery.setRoutes(); expect(matchQuery.driverRoute?.distance).toBe(350101); expect(matchQuery.passengerRoute?.distance).toBe(340102); }); it('should throw an exception if route is not found', async () => { - const matchQuery = new MatchQuery({ - driver: true, - passenger: false, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '01:05', - }, - ], - waypoints: [originWaypoint, destinationWaypoint], - }); - await expect( - matchQuery.setRoutes(mockRouteProvider), - ).rejects.toBeInstanceOf(Error); + const matchQuery = new MatchQuery( + { + driver: true, + passenger: false, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + await expect(matchQuery.setRoutes()).rejects.toBeInstanceOf(Error); }); }); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index 8c72f43..705a9a3 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -25,21 +25,29 @@ const destinationWaypoint: Waypoint = { country: 'France', }; -const matchQuery = new MatchQuery({ - algorithmType: AlgorithmType.PASSENGER_ORIENTED, - driver: false, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '07:05', - }, - ], - strict: false, - waypoints: [originWaypoint, destinationWaypoint], -}); +const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + { + getBasic: jest.fn().mockImplementation(() => ({ + duration: 6500, + distance: 89745, + })), + }, +); const mockMatcherRepository: AdRepositoryPort = { insertExtra: jest.fn(), diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index 9d607b7..cc0aa4b 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -24,21 +24,26 @@ const destinationWaypoint: Waypoint = { country: 'France', }; -const matchQuery = new MatchQuery({ - algorithmType: AlgorithmType.PASSENGER_ORIENTED, - driver: true, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '07:05', - }, - ], - strict: false, - waypoints: [originWaypoint, destinationWaypoint], -}); +const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + { + getBasic: jest.fn(), + }, +); const candidates: CandidateEntity[] = [ CandidateEntity.create({ diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 51770d9..2782557 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -24,21 +24,26 @@ const destinationWaypoint: Waypoint = { country: 'France', }; -const matchQuery = new MatchQuery({ - algorithmType: AlgorithmType.PASSENGER_ORIENTED, - driver: true, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-08-28', - toDate: '2023-08-28', - schedule: [ - { - time: '07:05', - }, - ], - strict: false, - waypoints: [originWaypoint, destinationWaypoint], -}); +const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + { + getBasic: jest.fn(), + }, +); const candidates: CandidateEntity[] = [ CandidateEntity.create({ diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index 4659916..0972a6f 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -25,27 +25,32 @@ const destinationWaypoint: Waypoint = { country: 'France', }; -const matchQuery = new MatchQuery({ - algorithmType: AlgorithmType.PASSENGER_ORIENTED, - driver: true, - passenger: true, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-06-21', - toDate: '2023-06-21', - useAzimuth: true, - azimuthMargin: 10, - useProportion: true, - proportion: 0.3, - schedule: [ - { - day: 3, - time: '07:05', - margin: 900, - }, - ], - strict: false, - waypoints: [originWaypoint, destinationWaypoint], -}); +const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-06-21', + toDate: '2023-06-21', + useAzimuth: true, + azimuthMargin: 10, + useProportion: true, + proportion: 0.3, + schedule: [ + { + day: 3, + time: '07:05', + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + { + getBasic: jest.fn(), + }, +); matchQuery.driverRoute = { distance: 150120, duration: 6540, diff --git a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 91020a9..cf614ed 100644 --- a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -1,4 +1,6 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; @@ -62,6 +64,10 @@ const mockQueryBus = { }), }; +const mockRouteProvider: RouteProviderPort = { + getBasic: jest.fn(), +}; + describe('Match Grpc Controller', () => { let matchGrpcController: MatchGrpcController; @@ -73,6 +79,10 @@ describe('Match Grpc Controller', () => { provide: QueryBus, useValue: mockQueryBus, }, + { + provide: AD_ROUTE_PROVIDER, + useValue: mockRouteProvider, + }, ], }).compile(); From 4e118603f36669343f0cc405890e208309fdedd6 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 18 Sep 2023 16:41:46 +0200 Subject: [PATCH 32/52] passenger oriented geo filter --- .../filter/passenger-oriented-geo.filter.ts | 4 +- .../selector/passenger-oriented.selector.ts | 6 + .../ad/core/domain/candidate.entity.ts | 23 +- src/modules/ad/core/domain/candidate.types.ts | 16 ++ .../tests/unit/core/candidate.entity.spec.ts | 207 ++++++++++++++++++ ...er-oriented-carpool-path-completer.spec.ts | 8 + .../passenger-oriented-geo-filter.spec.ts | 99 ++++----- 7 files changed, 303 insertions(+), 60 deletions(-) create mode 100644 src/modules/ad/tests/unit/core/candidate.entity.spec.ts diff --git a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts index ca6a558..79a311f 100644 --- a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts +++ b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts @@ -3,5 +3,7 @@ import { Filter } from './filter.abstract'; export class PassengerOrientedGeoFilter extends Filter { filter = async (candidates: CandidateEntity[]): Promise => - candidates; + candidates.filter((candidate: CandidateEntity) => + candidate.isDetourValid(), + ); } diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 7127eaa..9b70919 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -60,6 +60,12 @@ export class PassengerOrientedSelector extends Selector { adsRole.role == Role.PASSENGER ? (adEntity.getProps().driverDuration as number) : (this.query.driverRoute?.duration as number), + spacetimeDetourRatio: { + maxDistanceDetourRatio: this.query + .maxDetourDistanceRatio as number, + maxDurationDetourRatio: this.query + .maxDetourDurationRatio as number, + }, }), ), ) diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index d932eb9..89bcd84 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -10,15 +10,34 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; - setCarpoolPath = (waySteps: WayStepProps[]): void => { + setCarpoolPath = (waySteps: WayStepProps[]): CandidateEntity => { this.props.carpoolSteps = waySteps; + return this; }; - setMetrics = (distance: number, duration: number): void => { + setMetrics = (distance: number, duration: number): CandidateEntity => { this.props.distance = distance; this.props.duration = duration; + return this; }; + isDetourValid = (): boolean => + this._validateDistanceDetour() && this._validateDurationDetour(); + + private _validateDurationDetour = (): boolean => + this.props.duration + ? this.props.duration <= + this.props.driverDuration * + (1 + this.props.spacetimeDetourRatio.maxDurationDetourRatio) + : false; + + private _validateDistanceDetour = (): boolean => + this.props.distance + ? this.props.distance <= + this.props.driverDistance * + (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) + : false; + validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 84af6ef..d466a2f 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -12,6 +12,7 @@ export interface CandidateProps { carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) distance?: number; duration?: number; + spacetimeDetourRatio: SpacetimeDetourRatio; } // Properties that are needed for a Candidate creation @@ -22,6 +23,7 @@ export interface CreateCandidateProps { driverDuration: number; driverWaypoints: PointProps[]; passengerWaypoints: PointProps[]; + spacetimeDetourRatio: SpacetimeDetourRatio; } export enum Target { @@ -30,3 +32,17 @@ export enum Target { FINISH = 'FINISH', NEUTRAL = 'NEUTRAL', } + +export abstract class Validator { + abstract validate(): boolean; +} + +export type SpacetimeMetric = { + distance: number; + duration: number; +}; + +export type SpacetimeDetourRatio = { + maxDistanceDetourRatio: number; + maxDurationDetourRatio: number; +}; diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts new file mode 100644 index 0000000..fcf52c7 --- /dev/null +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -0,0 +1,207 @@ +import { Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; + +describe('Candidate entity', () => { + it('should create a new candidate entity', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }); + expect(candidateEntity.id.length).toBe(36); + }); + it('should set a candidate entity carpool path', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + role: Role.PASSENGER, + driverWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).setCarpoolPath([ + { + point: new Point({ + lat: 48.689445, + lon: 6.17651, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.START, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.START, + }), + ], + }, + { + point: new Point({ + lat: 48.8566, + lon: 2.3522, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.FINISH, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.FINISH, + }), + ], + }, + ]); + expect(candidateEntity.getProps().carpoolSteps).toHaveLength(2); + }); + it('should create a new candidate entity with spacetime metrics', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).setMetrics(352688, 14587); + expect(candidateEntity.getProps().distance).toBe(352688); + expect(candidateEntity.getProps().duration).toBe(14587); + }); + it('should not validate a candidate entity with exceeding distance detour', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).setMetrics(458690, 13980); + expect(candidateEntity.isDetourValid()).toBeFalsy(); + }); + it('should not validate a candidate entity with exceeding duration detour', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).setMetrics(352368, 18314); + expect(candidateEntity.isDetourValid()).toBeFalsy(); + }); +}); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index cc0aa4b..fdf5a0f 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -71,6 +71,10 @@ const candidates: CandidateEntity[] = [ ], driverDistance: 350145, driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, }), CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', @@ -97,6 +101,10 @@ const candidates: CandidateEntity[] = [ ], driverDistance: 350145, driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, }), ]; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 2782557..394717a 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -45,67 +45,52 @@ const matchQuery = new MatchQuery( }, ); -const candidates: CandidateEntity[] = [ - CandidateEntity.create({ - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - role: Role.DRIVER, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, - }, - { - lat: 48.84877, - lon: 2.398457, - }, - ], - passengerWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], - driverDistance: 350145, - driverDuration: 13548, - }), - CandidateEntity.create({ - id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', - role: Role.PASSENGER, - driverWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], - passengerWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], - driverDistance: 350145, - driverDuration: 13548, - }), -]; +const candidate: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, +}); describe('Passenger oriented geo filter', () => { - it('should filter candidates', async () => { + it('should not filter valid candidates', async () => { const passengerOrientedGeoFilter: PassengerOrientedGeoFilter = new PassengerOrientedGeoFilter(matchQuery); + candidate.isDetourValid = () => true; const filteredCandidates: CandidateEntity[] = - await passengerOrientedGeoFilter.filter(candidates); - expect(filteredCandidates.length).toBe(2); + await passengerOrientedGeoFilter.filter([candidate]); + expect(filteredCandidates.length).toBe(1); + }); + it('should filter invalid candidates', async () => { + const passengerOrientedGeoFilter: PassengerOrientedGeoFilter = + new PassengerOrientedGeoFilter(matchQuery); + candidate.isDetourValid = () => false; + const filteredCandidates: CandidateEntity[] = + await passengerOrientedGeoFilter.filter([candidate]); + expect(filteredCandidates.length).toBe(0); }); }); From 32d5ec25b93434782cb8ae8157f8e5ad00906bc2 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 18 Sep 2023 16:44:06 +0200 Subject: [PATCH 33/52] passenger oriented geo filter --- .../queries/match/filter/passenger-oriented-geo.filter.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts index 79a311f..7061ee0 100644 --- a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts +++ b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts @@ -1,6 +1,9 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Filter } from './filter.abstract'; +/** + * Filter candidates with unacceptable detour + */ export class PassengerOrientedGeoFilter extends Filter { filter = async (candidates: CandidateEntity[]): Promise => candidates.filter((candidate: CandidateEntity) => From 075a856d099a2e6ba15fe86585aa6ffcdc4a5f07 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 18 Sep 2023 17:09:19 +0200 Subject: [PATCH 34/52] detailed route provider --- src/modules/ad/ad.di-tokens.ts | 3 ++ src/modules/ad/ad.module.ts | 6 ++++ .../application/ports/route-provider.port.ts | 12 ++++++- .../match/completer/route.completer.ts | 31 +++++++++++++++---- .../ad/infrastructure/route-provider.ts | 9 ++++-- .../tests/unit/core/create-ad.service.spec.ts | 1 + .../unit/core/match.query-handler.spec.ts | 1 + .../ad/tests/unit/core/match.query.spec.ts | 1 + .../core/passenger-oriented-algorithm.spec.ts | 1 + ...er-oriented-carpool-path-completer.spec.ts | 1 + .../passenger-oriented-geo-filter.spec.ts | 1 + .../core/passenger-oriented-selector.spec.ts | 1 + .../unit/infrastructure/ad.repository.spec.ts | 1 + .../infrastructure/route-provider.spec.ts | 6 ++-- .../interface/match.grpc.controller.spec.ts | 1 + ...r.port.ts => get-route-controller.port.ts} | 2 +- .../queries/get-route/get-route.query.ts | 9 +++++- .../controllers/get-basic-route.controller.ts | 10 ++---- .../get-detailed-route.controller.ts | 27 ++++++++++++++++ 19 files changed, 103 insertions(+), 21 deletions(-) rename src/modules/geography/core/application/ports/{get-basic-route-controller.port.ts => get-route-controller.port.ts} (84%) create mode 100644 src/modules/geography/interface/controllers/get-detailed-route.controller.ts diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 4a69ae2..87c11ec 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -4,6 +4,9 @@ export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER'); export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol( 'AD_GET_BASIC_ROUTE_CONTROLLER', ); +export const AD_GET_DETAILED_ROUTE_CONTROLLER = Symbol( + 'AD_GET_DETAILED_ROUTE_CONTROLLER', +); export const AD_ROUTE_PROVIDER = Symbol('AD_ROUTE_PROVIDER'); export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 90610bd..bebd374 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -10,6 +10,7 @@ import { TIMEZONE_FINDER, TIME_CONVERTER, INPUT_DATETIME_TRANSFORMER, + AD_GET_DETAILED_ROUTE_CONTROLLER, } from './ad.di-tokens'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { AdRepository } from './infrastructure/ad.repository'; @@ -27,6 +28,7 @@ import { DefaultParamsProvider } from './infrastructure/default-params-provider' import { TimezoneFinder } from './infrastructure/timezone-finder'; import { TimeConverter } from './infrastructure/time-converter'; import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; +import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller'; const grpcControllers = [MatchGrpcController]; @@ -67,6 +69,10 @@ const adapters: Provider[] = [ provide: AD_GET_BASIC_ROUTE_CONTROLLER, useClass: GetBasicRouteController, }, + { + provide: AD_GET_DETAILED_ROUTE_CONTROLLER, + useClass: GetDetailedRouteController, + }, { provide: PARAMS_PROVIDER, useClass: DefaultParamsProvider, diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts index b016365..ca06709 100644 --- a/src/modules/ad/core/application/ports/route-provider.port.ts +++ b/src/modules/ad/core/application/ports/route-provider.port.ts @@ -3,7 +3,17 @@ import { Point } from '../types/point.type'; export interface RouteProviderPort { /** - * Get a basic route with points and overall duration / distance + * Get a basic route : + * - simple points (coordinates only) + * - overall duration + * - overall distance */ getBasic(waypoints: Point[]): Promise; + /** + * Get a detailed route : + * - detailed points (coordinates and time / distance to reach the point) + * - overall duration + * - overall distance + */ + getDetailed(waypoints: Point[]): Promise; } diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts index 2f516ec..c8e61a9 100644 --- a/src/modules/ad/core/application/queries/match/completer/route.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -15,12 +15,31 @@ export class RouteCompleter extends Completer { ): Promise => { await Promise.all( candidates.map(async (candidate: CandidateEntity) => { - const candidateRoute = await this.query.routeProvider.getBasic( - (candidate.getProps().carpoolSteps as WayStep[]).map( - (wayStep: WayStep) => wayStep.point, - ), - ); - candidate.setMetrics(candidateRoute.distance, candidateRoute.duration); + switch (this.type) { + case RouteCompleterType.BASIC: + const basicCandidateRoute = await this.query.routeProvider.getBasic( + (candidate.getProps().carpoolSteps as WayStep[]).map( + (wayStep: WayStep) => wayStep.point, + ), + ); + candidate.setMetrics( + basicCandidateRoute.distance, + basicCandidateRoute.duration, + ); + break; + case RouteCompleterType.DETAILED: + const detailedCandidateRoute = + await this.query.routeProvider.getBasic( + (candidate.getProps().carpoolSteps as WayStep[]).map( + (wayStep: WayStep) => wayStep.point, + ), + ); + candidate.setMetrics( + detailedCandidateRoute.distance, + detailedCandidateRoute.duration, + ); + break; + } return candidate; }), ); diff --git a/src/modules/ad/infrastructure/route-provider.ts b/src/modules/ad/infrastructure/route-provider.ts index b32317b..dac1d05 100644 --- a/src/modules/ad/infrastructure/route-provider.ts +++ b/src/modules/ad/infrastructure/route-provider.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { RouteProviderPort } from '../core/application/ports/route-provider.port'; -import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; +import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; import { Point, Route } from '@modules/geography/core/domain/route.types'; @@ -8,11 +8,16 @@ import { Point, Route } from '@modules/geography/core/domain/route.types'; export class RouteProvider implements RouteProviderPort { constructor( @Inject(AD_GET_BASIC_ROUTE_CONTROLLER) - private readonly getBasicRouteController: GetBasicRouteControllerPort, + private readonly getBasicRouteController: GetRouteControllerPort, ) {} getBasic = async (waypoints: Point[]): Promise => await this.getBasicRouteController.get({ waypoints, }); + + getDetailed = async (waypoints: Point[]): Promise => + await this.getBasicRouteController.get({ + waypoints, + }); } diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 4198e68..220bd18 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -93,6 +93,7 @@ const mockRouteProvider: RouteProviderPort = { }, ], })), + getDetailed: jest.fn(), }; describe('create-ad.service', () => { diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index c96bd3b..9fd930d 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -92,6 +92,7 @@ const mockRouteProvider: RouteProviderPort = { distanceAzimuth: 336544, points: [], })), + getDetailed: jest.fn(), }; describe('Match Query Handler', () => { diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts index 8c0c580..14f7415 100644 --- a/src/modules/ad/tests/unit/core/match.query.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -104,6 +104,7 @@ const mockRouteProvider: RouteProviderPort = { .mockImplementationOnce(() => { throw new Error(); }), + getDetailed: jest.fn(), }; describe('Match Query', () => { diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index 705a9a3..4e9748e 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -46,6 +46,7 @@ const matchQuery = new MatchQuery( duration: 6500, distance: 89745, })), + getDetailed: jest.fn(), }, ); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index fdf5a0f..b1235e3 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -42,6 +42,7 @@ const matchQuery = new MatchQuery( }, { getBasic: jest.fn(), + getDetailed: jest.fn(), }, ); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 394717a..331183c 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -42,6 +42,7 @@ const matchQuery = new MatchQuery( }, { getBasic: jest.fn(), + getDetailed: jest.fn(), }, ); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index 0972a6f..c3be07e 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -49,6 +49,7 @@ const matchQuery = new MatchQuery( }, { getBasic: jest.fn(), + getDetailed: jest.fn(), }, ); matchQuery.driverRoute = { diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index 064349c..8c68cf7 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -75,6 +75,7 @@ const mockDirectionEncoder: DirectionEncoderPort = { const mockRouteProvider: RouteProviderPort = { getBasic: jest.fn(), + getDetailed: jest.fn(), }; const mockPrismaService = { diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts index 82113c8..641d5b3 100644 --- a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts @@ -1,7 +1,7 @@ import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens'; import { Point } from '@modules/ad/core/application/types/point.type'; import { RouteProvider } from '@modules/ad/infrastructure/route-provider'; -import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; +import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; import { Route } from '@modules/geography/core/domain/route.types'; import { Test, TestingModule } from '@nestjs/testing'; @@ -14,7 +14,7 @@ const destinationPoint: Point = { lon: 2.3522, }; -const mockGetBasicRouteController: GetBasicRouteControllerPort = { +const mockGetBasicRouteController: GetRouteControllerPort = { get: jest.fn().mockImplementationOnce(() => ({ distance: 350101, duration: 14422, @@ -59,7 +59,7 @@ describe('Route provider', () => { expect(routeProvider).toBeDefined(); }); - it('should provide a route', async () => { + it('should provide a basic route', async () => { const route: Route = await routeProvider.getBasic([ originPoint, destinationPoint, diff --git a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index cf614ed..6f75433 100644 --- a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -66,6 +66,7 @@ const mockQueryBus = { const mockRouteProvider: RouteProviderPort = { getBasic: jest.fn(), + getDetailed: jest.fn(), }; describe('Match Grpc Controller', () => { diff --git a/src/modules/geography/core/application/ports/get-basic-route-controller.port.ts b/src/modules/geography/core/application/ports/get-route-controller.port.ts similarity index 84% rename from src/modules/geography/core/application/ports/get-basic-route-controller.port.ts rename to src/modules/geography/core/application/ports/get-route-controller.port.ts index a1f0bd4..0217143 100644 --- a/src/modules/geography/core/application/ports/get-basic-route-controller.port.ts +++ b/src/modules/geography/core/application/ports/get-route-controller.port.ts @@ -1,6 +1,6 @@ import { GetRouteRequestDto } from '@modules/geography/interface/controllers/dtos/get-route.request.dto'; import { RouteResponseDto } from '@modules/geography/interface/dtos/route.response.dto'; -export interface GetBasicRouteControllerPort { +export interface GetRouteControllerPort { get(data: GetRouteRequestDto): Promise; } diff --git a/src/modules/geography/core/application/queries/get-route/get-route.query.ts b/src/modules/geography/core/application/queries/get-route/get-route.query.ts index 2eecbc0..56e33d6 100644 --- a/src/modules/geography/core/application/queries/get-route/get-route.query.ts +++ b/src/modules/geography/core/application/queries/get-route/get-route.query.ts @@ -6,7 +6,14 @@ export class GetRouteQuery extends QueryBase { readonly waypoints: Point[]; readonly georouterSettings: GeorouterSettings; - constructor(waypoints: Point[], georouterSettings: GeorouterSettings) { + constructor( + waypoints: Point[], + georouterSettings: GeorouterSettings = { + detailedDistance: false, + detailedDuration: false, + points: true, + }, + ) { super(); this.waypoints = waypoints; this.georouterSettings = georouterSettings; diff --git a/src/modules/geography/interface/controllers/get-basic-route.controller.ts b/src/modules/geography/interface/controllers/get-basic-route.controller.ts index 3c14b10..b28b88e 100644 --- a/src/modules/geography/interface/controllers/get-basic-route.controller.ts +++ b/src/modules/geography/interface/controllers/get-basic-route.controller.ts @@ -5,10 +5,10 @@ import { RouteEntity } from '@modules/geography/core/domain/route.entity'; import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query'; import { RouteMapper } from '@modules/geography/route.mapper'; import { Controller } from '@nestjs/common'; -import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; +import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; @Controller() -export class GetBasicRouteController implements GetBasicRouteControllerPort { +export class GetBasicRouteController implements GetRouteControllerPort { constructor( private readonly queryBus: QueryBus, private readonly mapper: RouteMapper, @@ -16,11 +16,7 @@ export class GetBasicRouteController implements GetBasicRouteControllerPort { async get(data: GetRouteRequestDto): Promise { const route: RouteEntity = await this.queryBus.execute( - new GetRouteQuery(data.waypoints, { - detailedDistance: false, - detailedDuration: false, - points: true, - }), + new GetRouteQuery(data.waypoints), ); return this.mapper.toResponse(route); } diff --git a/src/modules/geography/interface/controllers/get-detailed-route.controller.ts b/src/modules/geography/interface/controllers/get-detailed-route.controller.ts new file mode 100644 index 0000000..34cf693 --- /dev/null +++ b/src/modules/geography/interface/controllers/get-detailed-route.controller.ts @@ -0,0 +1,27 @@ +import { QueryBus } from '@nestjs/cqrs'; +import { RouteResponseDto } from '../dtos/route.response.dto'; +import { GetRouteRequestDto } from './dtos/get-route.request.dto'; +import { RouteEntity } from '@modules/geography/core/domain/route.entity'; +import { GetRouteQuery } from '@modules/geography/core/application/queries/get-route/get-route.query'; +import { RouteMapper } from '@modules/geography/route.mapper'; +import { Controller } from '@nestjs/common'; +import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; + +@Controller() +export class GetDetailedRouteController implements GetRouteControllerPort { + constructor( + private readonly queryBus: QueryBus, + private readonly mapper: RouteMapper, + ) {} + + async get(data: GetRouteRequestDto): Promise { + const route: RouteEntity = await this.queryBus.execute( + new GetRouteQuery(data.waypoints, { + detailedDistance: true, + detailedDuration: true, + points: true, + }), + ); + return this.mapper.toResponse(route); + } +} From efea6fe13c82a4e3cd7b42ff9ad3c3f914170cb3 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 19 Sep 2023 10:54:23 +0200 Subject: [PATCH 35/52] improve tests --- .../match/completer/route.completer.ts | 2 +- .../ad/infrastructure/route-provider.ts | 9 +- .../unit/core/algorithm.abstract.spec.ts | 117 +++++++++++++ .../tests/unit/core/route.completer.spec.ts | 154 ++++++++++++++++++ .../infrastructure/route-provider.spec.ts | 42 ++++- .../get-detailed-route.controller.spec.ts | 63 +++++++ 6 files changed, 383 insertions(+), 4 deletions(-) create mode 100644 src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts create mode 100644 src/modules/ad/tests/unit/core/route.completer.spec.ts create mode 100644 src/modules/geography/tests/unit/interface/get-detailed-route.controller.spec.ts diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts index c8e61a9..3a61a20 100644 --- a/src/modules/ad/core/application/queries/match/completer/route.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -29,7 +29,7 @@ export class RouteCompleter extends Completer { break; case RouteCompleterType.DETAILED: const detailedCandidateRoute = - await this.query.routeProvider.getBasic( + await this.query.routeProvider.getDetailed( (candidate.getProps().carpoolSteps as WayStep[]).map( (wayStep: WayStep) => wayStep.point, ), diff --git a/src/modules/ad/infrastructure/route-provider.ts b/src/modules/ad/infrastructure/route-provider.ts index dac1d05..ada7160 100644 --- a/src/modules/ad/infrastructure/route-provider.ts +++ b/src/modules/ad/infrastructure/route-provider.ts @@ -1,7 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { RouteProviderPort } from '../core/application/ports/route-provider.port'; import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; -import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; +import { + AD_GET_BASIC_ROUTE_CONTROLLER, + AD_GET_DETAILED_ROUTE_CONTROLLER, +} from '../ad.di-tokens'; import { Point, Route } from '@modules/geography/core/domain/route.types'; @Injectable() @@ -9,6 +12,8 @@ export class RouteProvider implements RouteProviderPort { constructor( @Inject(AD_GET_BASIC_ROUTE_CONTROLLER) private readonly getBasicRouteController: GetRouteControllerPort, + @Inject(AD_GET_DETAILED_ROUTE_CONTROLLER) + private readonly getDetailedRouteController: GetRouteControllerPort, ) {} getBasic = async (waypoints: Point[]): Promise => @@ -17,7 +22,7 @@ export class RouteProvider implements RouteProviderPort { }); getDetailed = async (waypoints: Point[]): Promise => - await this.getBasicRouteController.get({ + await this.getDetailedRouteController.get({ waypoints, }); } diff --git a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts new file mode 100644 index 0000000..9c0c01d --- /dev/null +++ b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts @@ -0,0 +1,117 @@ +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; +import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { + Algorithm, + Selector, +} from '@modules/ad/core/application/queries/match/algorithm.abstract'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const mockRouteProvider: RouteProviderPort = { + getBasic: jest.fn(), + getDetailed: jest.fn(), +}; + +const matchQuery = new MatchQuery( + { + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '01:05', + }, + ], + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, +); + +const mockAdRepository: AdRepositoryPort = { + insertExtra: jest.fn(), + findOneById: jest.fn(), + findOne: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + updateWhere: jest.fn(), + delete: jest.fn(), + count: jest.fn(), + healthCheck: jest.fn(), + getCandidateAds: jest.fn(), +}; + +class SomeSelector extends Selector { + select = async (): Promise => [ + CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }), + ]; +} + +class SomeAlgorithm extends Algorithm { + constructor( + protected readonly query: MatchQuery, + protected readonly repository: AdRepositoryPort, + ) { + super(query, repository); + this.selector = new SomeSelector(query, repository); + this.processors = []; + } +} + +describe('Abstract Algorithm', () => { + it('should return matches', async () => { + const someAlgorithm = new SomeAlgorithm(matchQuery, mockAdRepository); + const matches: MatchEntity[] = await someAlgorithm.match(); + expect(matches).toHaveLength(1); + }); +}); diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts new file mode 100644 index 0000000..5aff29e --- /dev/null +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -0,0 +1,154 @@ +import { + RouteCompleter, + RouteCompleterType, +} from '@modules/ad/core/application/queries/match/completer/route.completer'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + { + getBasic: jest.fn().mockImplementation(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })), + getDetailed: jest.fn().mockImplementation(() => ({ + distance: 350102, + duration: 14423, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })), + }, +); + +const candidate: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, +}).setCarpoolPath([ + { + point: new Point({ + lat: 48.689445, + lon: 6.17651, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.START, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.START, + }), + ], + }, + { + point: new Point({ + lat: 48.8566, + lon: 2.3522, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.FINISH, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.FINISH, + }), + ], + }, +]); + +describe('Route completer', () => { + it('should complete candidates with basic setting', async () => { + const routeCompleter: RouteCompleter = new RouteCompleter( + matchQuery, + RouteCompleterType.BASIC, + ); + const completedCandidates: CandidateEntity[] = + await routeCompleter.complete([candidate]); + expect(completedCandidates.length).toBe(1); + expect(completedCandidates[0].getProps().distance).toBe(350101); + }); + it('should complete candidates with detailed setting', async () => { + const routeCompleter: RouteCompleter = new RouteCompleter( + matchQuery, + RouteCompleterType.DETAILED, + ); + const completedCandidates: CandidateEntity[] = + await routeCompleter.complete([candidate]); + expect(completedCandidates.length).toBe(1); + expect(completedCandidates[0].getProps().distance).toBe(350102); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts index 641d5b3..6e8ec9a 100644 --- a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts @@ -1,4 +1,7 @@ -import { AD_GET_BASIC_ROUTE_CONTROLLER } from '@modules/ad/ad.di-tokens'; +import { + AD_GET_BASIC_ROUTE_CONTROLLER, + AD_GET_DETAILED_ROUTE_CONTROLLER, +} from '@modules/ad/ad.di-tokens'; import { Point } from '@modules/ad/core/application/types/point.type'; import { RouteProvider } from '@modules/ad/infrastructure/route-provider'; import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; @@ -38,6 +41,30 @@ const mockGetBasicRouteController: GetRouteControllerPort = { })), }; +const mockGetDetailedRouteController: GetRouteControllerPort = { + get: jest.fn().mockImplementationOnce(() => ({ + distance: 350102, + duration: 14423, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [ + { + lon: 6.1765102, + lat: 48.689445, + }, + { + lon: 4.984578, + lat: 48.725687, + }, + { + lon: 2.3522, + lat: 48.8566, + }, + ], + })), +}; + describe('Route provider', () => { let routeProvider: RouteProvider; @@ -49,6 +76,10 @@ describe('Route provider', () => { provide: AD_GET_BASIC_ROUTE_CONTROLLER, useValue: mockGetBasicRouteController, }, + { + provide: AD_GET_DETAILED_ROUTE_CONTROLLER, + useValue: mockGetDetailedRouteController, + }, ], }).compile(); @@ -67,4 +98,13 @@ describe('Route provider', () => { expect(route.distance).toBe(350101); expect(route.duration).toBe(14422); }); + + it('should provide a detailed route', async () => { + const route: Route = await routeProvider.getDetailed([ + originPoint, + destinationPoint, + ]); + expect(route.distance).toBe(350102); + expect(route.duration).toBe(14423); + }); }); diff --git a/src/modules/geography/tests/unit/interface/get-detailed-route.controller.spec.ts b/src/modules/geography/tests/unit/interface/get-detailed-route.controller.spec.ts new file mode 100644 index 0000000..e61e04e --- /dev/null +++ b/src/modules/geography/tests/unit/interface/get-detailed-route.controller.spec.ts @@ -0,0 +1,63 @@ +import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller'; +import { RouteMapper } from '@modules/geography/route.mapper'; +import { QueryBus } from '@nestjs/cqrs'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockQueryBus = { + execute: jest.fn(), +}; + +const mockRouteMapper = { + toPersistence: jest.fn(), + toDomain: jest.fn(), + toResponse: jest.fn(), +}; + +describe('Get Detailed Route Controller', () => { + let getDetailedRouteController: GetDetailedRouteController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: QueryBus, + useValue: mockQueryBus, + }, + { + provide: RouteMapper, + useValue: mockRouteMapper, + }, + GetDetailedRouteController, + ], + }).compile(); + + getDetailedRouteController = module.get( + GetDetailedRouteController, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(getDetailedRouteController).toBeDefined(); + }); + + it('should get a route', async () => { + jest.spyOn(mockQueryBus, 'execute'); + await getDetailedRouteController.get({ + waypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + }); + expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); + }); +}); From 1f1502a62377fd22d7f1bbefb077d5bfb2481d19 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 19 Sep 2023 12:33:48 +0200 Subject: [PATCH 36/52] fix geodesic error on azimuth --- .../queries/match/algorithm.abstract.ts | 2 +- .../match/passenger-oriented-algorithm.ts | 1 + .../core/application/ports/geodesic.port.ts | 2 ++ src/modules/geography/geography.module.ts | 9 ++++- .../geography/infrastructure/geodesic.ts | 33 ++++++++++++++++++- .../infrastructure/graphhopper-georouter.ts | 6 ++-- .../unit/infrastructure/geodesic.spec.ts | 22 +++++++++++++ .../graphhopper-georouter.spec.ts | 2 ++ 8 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index faeb9bc..6915366 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -20,7 +20,7 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } - // console.log(JSON.stringify(this.candidates, null, 2)); + console.log(JSON.stringify(this.candidates, null, 2)); return this.candidates.map((candidate: CandidateEntity) => MatchEntity.create({ adId: candidate.id }), ); diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index d9bc5af..618a5dc 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -20,6 +20,7 @@ export class PassengerOrientedAlgorithm extends Algorithm { new PassengerOrientedCarpoolPathCompleter(query), new RouteCompleter(query, RouteCompleterType.BASIC), new PassengerOrientedGeoFilter(query), + new RouteCompleter(query, RouteCompleterType.DETAILED), ]; } } diff --git a/src/modules/geography/core/application/ports/geodesic.port.ts b/src/modules/geography/core/application/ports/geodesic.port.ts index 77b45ba..6da4a05 100644 --- a/src/modules/geography/core/application/ports/geodesic.port.ts +++ b/src/modules/geography/core/application/ports/geodesic.port.ts @@ -8,4 +8,6 @@ export interface GeodesicPort { azimuth: number; distance: number; }; + distance(lon1: number, lat1: number, lon2: number, lat2: number): number; + azimuth(lon1: number, lat1: number, lon2: number, lat2: number): number; } diff --git a/src/modules/geography/geography.module.ts b/src/modules/geography/geography.module.ts index 6c04e79..77a6d58 100644 --- a/src/modules/geography/geography.module.ts +++ b/src/modules/geography/geography.module.ts @@ -14,6 +14,7 @@ import { Geodesic } from './infrastructure/geodesic'; import { GraphhopperGeorouter } from './infrastructure/graphhopper-georouter'; import { HttpModule } from '@nestjs/axios'; import { GetRouteQueryHandler } from './core/application/queries/get-route/get-route.query-handler'; +import { GetDetailedRouteController } from './interface/controllers/get-detailed-route.controller'; const queryHandlers: Provider[] = [GetRouteQueryHandler]; @@ -37,11 +38,17 @@ const adapters: Provider[] = [ useClass: Geodesic, }, GetBasicRouteController, + GetDetailedRouteController, ]; @Module({ imports: [CqrsModule, HttpModule], providers: [...queryHandlers, ...mappers, ...adapters], - exports: [RouteMapper, DIRECTION_ENCODER, GetBasicRouteController], + exports: [ + RouteMapper, + DIRECTION_ENCODER, + GetBasicRouteController, + GetDetailedRouteController, + ], }) export class GeographyModule {} diff --git a/src/modules/geography/infrastructure/geodesic.ts b/src/modules/geography/infrastructure/geodesic.ts index f7ac3f1..5655585 100644 --- a/src/modules/geography/infrastructure/geodesic.ts +++ b/src/modules/geography/infrastructure/geodesic.ts @@ -22,7 +22,38 @@ export class Geodesic implements GeodesicPort { lat2, lon2, ); - if (!azimuth || !distance) throw new Error('Azimuth not found'); + if (!azimuth || !distance) + throw new Error( + `Inverse not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`, + ); return { azimuth, distance }; }; + + azimuth = ( + lon1: number, + lat1: number, + lon2: number, + lat2: number, + ): number => { + const { azi2: azimuth } = this.geod.Inverse(lat1, lon1, lat2, lon2); + if (!azimuth) + throw new Error( + `Azimuth not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`, + ); + return azimuth; + }; + + distance = ( + lon1: number, + lat1: number, + lon2: number, + lat2: number, + ): number => { + const { s12: distance } = this.geod.Inverse(lat1, lon1, lat2, lon2); + if (!distance) + throw new Error( + `Distance not found for coordinates ${lon1} ${lat1} / ${lon2} ${lat2}`, + ); + return distance; + }; } diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts index 891ec77..a12b526 100644 --- a/src/modules/geography/infrastructure/graphhopper-georouter.ts +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -192,14 +192,14 @@ export class GraphhopperGeorouter implements GeorouterPort { .filter((element) => element.index == -1); for (const index in points) { for (const missedWaypoint of missedWaypoints) { - const inverse = this.geodesic.inverse( + const distance = this.geodesic.distance( missedWaypoint.waypoint[0], missedWaypoint.waypoint[1], points[index][0], points[index][1], ); - if (inverse.distance < missedWaypoint.distance) { - missedWaypoint.distance = inverse.distance; + if (distance < missedWaypoint.distance) { + missedWaypoint.distance = distance; missedWaypoint.nearest = parseInt(index); } } diff --git a/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts b/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts index c094a0e..82f29f7 100644 --- a/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/geodesic.spec.ts @@ -11,4 +11,26 @@ describe('Matcher geodesic', () => { expect(Math.round(inv.azimuth as number)).toBe(45); expect(Math.round(inv.distance as number)).toBe(156900); }); + it('should get azimuth value', () => { + const geodesic: Geodesic = new Geodesic(); + const azimuth = geodesic.azimuth(0, 0, 1, 1); + expect(Math.round(azimuth as number)).toBe(45); + }); + it('should get distance value', () => { + const geodesic: Geodesic = new Geodesic(); + const distance = geodesic.distance(0, 0, 1, 1); + expect(Math.round(distance as number)).toBe(156900); + }); + it('should throw an exception if inverse fails', () => { + const geodesic: Geodesic = new Geodesic(); + expect(() => { + geodesic.inverse(7.74547, 48.583035, 7.74547, 48.583036); + }).toThrow(); + }); + it('should throw an exception if azimuth fails', () => { + const geodesic: Geodesic = new Geodesic(); + expect(() => { + geodesic.azimuth(7.74547, 48.583035, 7.74547, 48.583036); + }).toThrow(); + }); }); diff --git a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts index 419b1bd..dad71fb 100644 --- a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts @@ -253,6 +253,8 @@ const mockGeodesic: GeodesicPort = { azimuth: 45, distance: 50000, })), + azimuth: jest.fn().mockImplementation(() => 45), + distance: jest.fn().mockImplementation(() => 50000), }; const mockDefaultParamsProvider: DefaultParamsProviderPort = { From 7cac8cbef9e1153c7e936eed100a7fc3b16869fe Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 19 Sep 2023 14:30:56 +0200 Subject: [PATCH 37/52] add steps to route response --- .../geography/core/domain/route.entity.ts | 1 + .../geography/core/domain/route.types.ts | 2 ++ .../domain/value-objects/step.value-object.ts | 6 ++--- src/modules/geography/route.mapper.ts | 1 + .../graphhopper-georouter.spec.ts | 27 +++++++++++++++++++ 5 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/modules/geography/core/domain/route.entity.ts b/src/modules/geography/core/domain/route.entity.ts index 7a18c96..be0613d 100644 --- a/src/modules/geography/core/domain/route.entity.ts +++ b/src/modules/geography/core/domain/route.entity.ts @@ -19,6 +19,7 @@ export class RouteEntity extends AggregateRoot { backAzimuth: route.backAzimuth, distanceAzimuth: route.distanceAzimuth, points: route.points, + steps: route.steps, }; return new RouteEntity({ id: v4(), diff --git a/src/modules/geography/core/domain/route.types.ts b/src/modules/geography/core/domain/route.types.ts index 87d9130..e478575 100644 --- a/src/modules/geography/core/domain/route.types.ts +++ b/src/modules/geography/core/domain/route.types.ts @@ -1,6 +1,7 @@ import { GeorouterPort } from '../application/ports/georouter.port'; import { GeorouterSettings } from '../application/types/georouter-settings.type'; import { PointProps } from './value-objects/point.value-object'; +import { StepProps } from './value-objects/step.value-object'; // All properties that a Route has export interface RouteProps { @@ -10,6 +11,7 @@ export interface RouteProps { backAzimuth: number; distanceAzimuth: number; points: PointProps[]; + steps?: StepProps[]; } // Properties that are needed for a Route creation diff --git a/src/modules/geography/core/domain/value-objects/step.value-object.ts b/src/modules/geography/core/domain/value-objects/step.value-object.ts index 5c04757..0c6d8df 100644 --- a/src/modules/geography/core/domain/value-objects/step.value-object.ts +++ b/src/modules/geography/core/domain/value-objects/step.value-object.ts @@ -8,7 +8,7 @@ import { PointProps } from './point.value-object'; export interface StepProps extends PointProps { duration: number; - distance: number; + distance?: number; } export class Step extends ValueObject { @@ -16,7 +16,7 @@ export class Step extends ValueObject { return this.props.duration; } - get distance(): number { + get distance(): number | undefined { return this.props.distance; } @@ -33,7 +33,7 @@ export class Step extends ValueObject { throw new ArgumentInvalidException( 'duration must be greater than or equal to 0', ); - if (props.distance < 0) + if (props.distance !== undefined && props.distance < 0) throw new ArgumentInvalidException( 'distance must be greater than or equal to 0', ); diff --git a/src/modules/geography/route.mapper.ts b/src/modules/geography/route.mapper.ts index 6353bad..43728f9 100644 --- a/src/modules/geography/route.mapper.ts +++ b/src/modules/geography/route.mapper.ts @@ -22,6 +22,7 @@ export class RouteMapper response.backAzimuth = Math.round(entity.getProps().backAzimuth); response.distanceAzimuth = Math.round(entity.getProps().distanceAzimuth); response.points = entity.getProps().points; + response.steps = entity.getProps().steps; return response; }; } diff --git a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts index dad71fb..ce9dec2 100644 --- a/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts +++ b/src/modules/geography/tests/unit/infrastructure/graphhopper-georouter.spec.ts @@ -26,6 +26,11 @@ const mockHttpService = { .mockImplementationOnce(() => { return throwError(() => 'Router unavailable'); }) + .mockImplementationOnce(() => { + return of({ + status: 200, + }); + }) .mockImplementationOnce(() => { return of({ status: 200, @@ -338,6 +343,28 @@ describe('Graphhopper Georouter', () => { ).rejects.toBeInstanceOf(GeorouterUnavailableException); }); + it('should fail if georouter response is corrupted', async () => { + await expect( + graphhopperGeorouter.route( + [ + { + lon: 0, + lat: 0, + }, + { + lon: 1, + lat: 1, + }, + ], + { + detailedDistance: false, + detailedDuration: false, + points: false, + }, + ), + ).rejects.toBeInstanceOf(GeorouterUnavailableException); + }); + it('should create a basic route', async () => { const route: Route = await graphhopperGeorouter.route( [ From 996759d0013baf0fd2f106bfccc8ce7ab444dece Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 19 Sep 2023 14:59:31 +0200 Subject: [PATCH 38/52] add schedule in candidate --- .../selector/passenger-oriented.selector.ts | 18 ++++- src/modules/ad/core/domain/candidate.types.ts | 5 ++ .../unit/core/algorithm.abstract.spec.ts | 14 ++++ .../tests/unit/core/candidate.entity.spec.ts | 70 +++++++++++++++++++ ...er-oriented-carpool-path-completer.spec.ts | 28 ++++++++ .../passenger-oriented-geo-filter.spec.ts | 14 ++++ .../tests/unit/core/route.completer.spec.ts | 14 ++++ 7 files changed, 162 insertions(+), 1 deletion(-) diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 9b70919..7768fba 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -1,10 +1,10 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Selector } from '../algorithm.abstract'; -import { ScheduleItem } from '../match.query'; import { Waypoint } from '../../../types/waypoint.type'; import { Point } from '../../../types/point.type'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object'; export class PassengerOrientedSelector extends Selector { select = async (): Promise => { @@ -60,6 +60,22 @@ export class PassengerOrientedSelector extends Selector { adsRole.role == Role.PASSENGER ? (adEntity.getProps().driverDuration as number) : (this.query.driverRoute?.duration as number), + driverSchedule: + adsRole.role == Role.PASSENGER + ? adEntity.getProps().schedule + : this.query.schedule.map((scheduleItem: ScheduleItem) => ({ + day: scheduleItem.day as number, + time: scheduleItem.time, + margin: scheduleItem.margin as number, + })), + passengerSchedule: + adsRole.role == Role.DRIVER + ? adEntity.getProps().schedule + : this.query.schedule.map((scheduleItem: ScheduleItem) => ({ + day: scheduleItem.day as number, + time: scheduleItem.time, + margin: scheduleItem.margin as number, + })), spacetimeDetourRatio: { maxDistanceDetourRatio: this.query .maxDetourDistanceRatio as number, diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index d466a2f..c387860 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,5 +1,6 @@ import { Role } from './ad.types'; import { PointProps } from './value-objects/point.value-object'; +import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { WayStepProps } from './value-objects/waystep.value-object'; // All properties that a Candidate has @@ -12,6 +13,8 @@ export interface CandidateProps { carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) distance?: number; duration?: number; + driverSchedule: ScheduleItemProps[]; + passengerSchedule: ScheduleItemProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; } @@ -23,6 +26,8 @@ export interface CreateCandidateProps { driverDuration: number; driverWaypoints: PointProps[]; passengerWaypoints: PointProps[]; + driverSchedule: ScheduleItemProps[]; + passengerSchedule: ScheduleItemProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; } diff --git a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts index 9c0c01d..c9757ce 100644 --- a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts +++ b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts @@ -89,6 +89,20 @@ class SomeSelector extends Selector { ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts index fcf52c7..c70d573 100644 --- a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -31,6 +31,20 @@ describe('Candidate entity', () => { ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, @@ -64,6 +78,20 @@ describe('Candidate entity', () => { ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, @@ -130,6 +158,20 @@ describe('Candidate entity', () => { ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, @@ -164,6 +206,20 @@ describe('Candidate entity', () => { ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, @@ -197,6 +253,20 @@ describe('Candidate entity', () => { ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index b1235e3..a7f0a90 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -72,6 +72,20 @@ const candidates: CandidateEntity[] = [ ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, @@ -102,6 +116,20 @@ const candidates: CandidateEntity[] = [ ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 331183c..1155369 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -71,6 +71,20 @@ const candidate: CandidateEntity = CandidateEntity.create({ ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index 5aff29e..b64cdf1 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -91,6 +91,20 @@ const candidate: CandidateEntity = CandidateEntity.create({ ], driverDistance: 350145, driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], spacetimeDetourRatio: { maxDistanceDetourRatio: 0.3, maxDurationDetourRatio: 0.3, From 6b6a169dee34147059e2e0bdb328873638af8586 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 19 Sep 2023 16:49:35 +0200 Subject: [PATCH 39/52] add actorTime --- .../application/ports/route-provider.port.ts | 2 +- .../match/completer/journey.completer.ts | 9 + .../match/completer/route.completer.ts | 16 +- .../application/queries/match/match.query.ts | 2 +- .../ad/core/application/types/route.type.ts | 12 ++ .../ad/core/application/types/step.type.ts | 6 + .../ad/core/domain/candidate.entity.ts | 14 +- src/modules/ad/core/domain/candidate.types.ts | 8 +- .../domain/carpool-path-creator.service.ts | 160 ++++++++++-------- .../ad/core/domain/path-creator.service.ts | 2 +- .../value-objects/actor-time.value-object.ts | 51 ++++++ ...object.ts => carpool-step.value-object.ts} | 8 +- .../value-objects/journey.value-object.ts | 42 +++++ .../schedule-item.value-object.ts | 1 - .../domain/value-objects/step.value-object.ts | 41 +++++ .../core/carpool-path-creator.service.spec.ts | 72 ++++---- ...c.ts => carpool-step.value-object.spec.ts} | 18 +- .../tests/unit/core/journey.completer.spec.ts | 151 +++++++++++++++++ .../tests/unit/core/route.completer.spec.ts | 7 +- .../infrastructure/route-provider.spec.ts | 2 +- 20 files changed, 481 insertions(+), 143 deletions(-) create mode 100644 src/modules/ad/core/application/queries/match/completer/journey.completer.ts create mode 100644 src/modules/ad/core/application/types/route.type.ts create mode 100644 src/modules/ad/core/application/types/step.type.ts create mode 100644 src/modules/ad/core/domain/value-objects/actor-time.value-object.ts rename src/modules/ad/core/domain/value-objects/{waystep.value-object.ts => carpool-step.value-object.ts} (78%) create mode 100644 src/modules/ad/core/domain/value-objects/journey.value-object.ts create mode 100644 src/modules/ad/core/domain/value-objects/step.value-object.ts rename src/modules/ad/tests/unit/core/{waystep.value-object.spec.ts => carpool-step.value-object.spec.ts} (76%) create mode 100644 src/modules/ad/tests/unit/core/journey.completer.spec.ts diff --git a/src/modules/ad/core/application/ports/route-provider.port.ts b/src/modules/ad/core/application/ports/route-provider.port.ts index ca06709..2087fba 100644 --- a/src/modules/ad/core/application/ports/route-provider.port.ts +++ b/src/modules/ad/core/application/ports/route-provider.port.ts @@ -1,5 +1,5 @@ -import { Route } from '@modules/geography/core/domain/route.types'; import { Point } from '../types/point.type'; +import { Route } from '../types/route.type'; export interface RouteProviderPort { /** diff --git a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts new file mode 100644 index 0000000..ac4ae5f --- /dev/null +++ b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts @@ -0,0 +1,9 @@ +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { Completer } from './completer.abstract'; + +export class JourneyCompleter extends Completer { + complete = async ( + candidates: CandidateEntity[], + ): Promise => + candidates.map((candidate: CandidateEntity) => candidate.createJourney()); +} diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts index 3a61a20..96dc9f1 100644 --- a/src/modules/ad/core/application/queries/match/completer/route.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -1,7 +1,8 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Completer } from './completer.abstract'; import { MatchQuery } from '../match.query'; -import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; +import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object'; +import { Step } from '../../../types/step.type'; export class RouteCompleter extends Completer { protected readonly type: RouteCompleterType; @@ -18,8 +19,8 @@ export class RouteCompleter extends Completer { switch (this.type) { case RouteCompleterType.BASIC: const basicCandidateRoute = await this.query.routeProvider.getBasic( - (candidate.getProps().carpoolSteps as WayStep[]).map( - (wayStep: WayStep) => wayStep.point, + (candidate.getProps().carpoolSteps as CarpoolStep[]).map( + (carpoolStep: CarpoolStep) => carpoolStep.point, ), ); candidate.setMetrics( @@ -30,14 +31,11 @@ export class RouteCompleter extends Completer { case RouteCompleterType.DETAILED: const detailedCandidateRoute = await this.query.routeProvider.getDetailed( - (candidate.getProps().carpoolSteps as WayStep[]).map( - (wayStep: WayStep) => wayStep.point, + (candidate.getProps().carpoolSteps as CarpoolStep[]).map( + (carpoolStep: CarpoolStep) => carpoolStep.point, ), ); - candidate.setMetrics( - detailedCandidateRoute.distance, - detailedCandidateRoute.duration, - ); + candidate.setSteps(detailedCandidateRoute.steps as Step[]); break; } return candidate; diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 8f96427..337ae7c 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -5,7 +5,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; import { RouteProviderPort } from '../../ports/route-provider.port'; -import { Route } from '@modules/geography/core/domain/route.types'; import { Path, PathCreator, @@ -13,6 +12,7 @@ import { TypedRoute, } from '@modules/ad/core/domain/path-creator.service'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; +import { Route } from '../../types/route.type'; export class MatchQuery extends QueryBase { driver?: boolean; diff --git a/src/modules/ad/core/application/types/route.type.ts b/src/modules/ad/core/application/types/route.type.ts new file mode 100644 index 0000000..297bc71 --- /dev/null +++ b/src/modules/ad/core/application/types/route.type.ts @@ -0,0 +1,12 @@ +import { Point } from './point.type'; +import { Step } from './step.type'; + +export type Route = { + distance: number; + duration: number; + fwdAzimuth: number; + backAzimuth: number; + distanceAzimuth: number; + points: Point[]; + steps?: Step[]; +}; diff --git a/src/modules/ad/core/application/types/step.type.ts b/src/modules/ad/core/application/types/step.type.ts new file mode 100644 index 0000000..c9e9b7b --- /dev/null +++ b/src/modules/ad/core/application/types/step.type.ts @@ -0,0 +1,6 @@ +import { Point } from './point.type'; + +export type Step = Point & { + duration: number; + distance?: number; +}; diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 89bcd84..7e4b1ad 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -1,6 +1,7 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { CandidateProps, CreateCandidateProps } from './candidate.types'; -import { WayStepProps } from './value-objects/waystep.value-object'; +import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; +import { StepProps } from './value-objects/step.value-object'; export class CandidateEntity extends AggregateRoot { protected readonly _id: AggregateID; @@ -10,8 +11,8 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; - setCarpoolPath = (waySteps: WayStepProps[]): CandidateEntity => { - this.props.carpoolSteps = waySteps; + setCarpoolPath = (carpoolSteps: CarpoolStepProps[]): CandidateEntity => { + this.props.carpoolSteps = carpoolSteps; return this; }; @@ -21,9 +22,16 @@ export class CandidateEntity extends AggregateRoot { return this; }; + setSteps = (steps: StepProps[]): CandidateEntity => { + this.props.steps = steps; + return this; + }; + isDetourValid = (): boolean => this._validateDistanceDetour() && this._validateDurationDetour(); + createJourney = (): CandidateEntity => this; + private _validateDurationDetour = (): boolean => this.props.duration ? this.props.duration <= diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index c387860..3a68d06 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,7 +1,9 @@ import { Role } from './ad.types'; import { PointProps } from './value-objects/point.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; -import { WayStepProps } from './value-objects/waystep.value-object'; +import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; +import { JourneyProps } from './value-objects/journey.value-object'; +import { StepProps } from './value-objects/step.value-object'; // All properties that a Candidate has export interface CandidateProps { @@ -10,11 +12,13 @@ export interface CandidateProps { passengerWaypoints: PointProps[]; driverDistance: number; driverDuration: number; - carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) + carpoolSteps?: CarpoolStepProps[]; distance?: number; duration?: number; + steps?: StepProps[]; driverSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[]; + journeys?: JourneyProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; } diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index bf1f5ae..43f61dd 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -3,7 +3,7 @@ import { Target } from './candidate.types'; import { CarpoolPathCreatorException } from './match.errors'; import { Actor } from './value-objects/actor.value-object'; import { Point } from './value-objects/point.value-object'; -import { WayStep } from './value-objects/waystep.value-object'; +import { CarpoolStep } from './value-objects/carpool-step.value-object'; export class CarpoolPathCreator { private PRECISION = 5; @@ -23,29 +23,35 @@ export class CarpoolPathCreator { } /** - * Creates a path (a list of waysteps) between driver waypoints + * Creates a path (a list of carpoolSteps) between driver waypoints and passenger waypoints respecting the order of the driver waypoints Inspired by : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment */ - public carpoolPath = (): WayStep[] => + public carpoolPath = (): CarpoolStep[] => this._consolidate( - this._mixedWaysteps(this._driverWaysteps(), this._passengerWaysteps()), + this._mixedCarpoolSteps( + this._driverCarpoolSteps(), + this._passengerCarpoolSteps(), + ), ); - private _mixedWaysteps = ( - driverWaysteps: WayStep[], - passengerWaysteps: WayStep[], - ): WayStep[] => - driverWaysteps.length == 2 - ? this._simpleMixedWaysteps(driverWaysteps, passengerWaysteps) - : this._complexMixedWaysteps(driverWaysteps, passengerWaysteps); + private _mixedCarpoolSteps = ( + driverCarpoolSteps: CarpoolStep[], + passengerCarpoolSteps: CarpoolStep[], + ): CarpoolStep[] => + driverCarpoolSteps.length == 2 + ? this._simpleMixedCarpoolSteps(driverCarpoolSteps, passengerCarpoolSteps) + : this._complexMixedCarpoolSteps( + driverCarpoolSteps, + passengerCarpoolSteps, + ); - private _driverWaysteps = (): WayStep[] => + private _driverCarpoolSteps = (): CarpoolStep[] => this.driverWaypoints.map( (waypoint: Point, index: number) => - new WayStep({ + new CarpoolStep({ point: new Point({ lon: waypoint.lon, lat: waypoint.lat, @@ -60,13 +66,13 @@ export class CarpoolPathCreator { ); /** - * Creates the passenger waysteps with original passenger waypoints, adding driver waypoints that are the same + * Creates the passenger carpoolSteps with original passenger waypoints, adding driver waypoints that are the same */ - private _passengerWaysteps = (): WayStep[] => { - const waysteps: WayStep[] = []; + private _passengerCarpoolSteps = (): CarpoolStep[] => { + const carpoolSteps: CarpoolStep[] = []; this.passengerWaypoints.forEach( (passengerWaypoint: Point, index: number) => { - const waystep: WayStep = new WayStep({ + const carpoolStep: CarpoolStep = new CarpoolStep({ point: new Point({ lon: passengerWaypoint.lon, lat: passengerWaypoint.lat, @@ -83,73 +89,78 @@ export class CarpoolPathCreator { passengerWaypoint.isSame(driverWaypoint), ).length == 0 ) { - waystep.actors.push( + carpoolStep.actors.push( new Actor({ role: Role.DRIVER, target: Target.NEUTRAL, }), ); } - waysteps.push(waystep); + carpoolSteps.push(carpoolStep); }, ); - return waysteps; + return carpoolSteps; }; - private _simpleMixedWaysteps = ( - driverWaysteps: WayStep[], - passengerWaysteps: WayStep[], - ): WayStep[] => [driverWaysteps[0], ...passengerWaysteps, driverWaysteps[1]]; + private _simpleMixedCarpoolSteps = ( + driverCarpoolSteps: CarpoolStep[], + passengerCarpoolSteps: CarpoolStep[], + ): CarpoolStep[] => [ + driverCarpoolSteps[0], + ...passengerCarpoolSteps, + driverCarpoolSteps[1], + ]; - private _complexMixedWaysteps = ( - driverWaysteps: WayStep[], - passengerWaysteps: WayStep[], - ): WayStep[] => { - let mixedWaysteps: WayStep[] = [...driverWaysteps]; + private _complexMixedCarpoolSteps = ( + driverCarpoolSteps: CarpoolStep[], + passengerCarpoolSteps: CarpoolStep[], + ): CarpoolStep[] => { + let mixedCarpoolSteps: CarpoolStep[] = [...driverCarpoolSteps]; const originInsertIndex: number = this._insertIndex( - passengerWaysteps[0], - driverWaysteps, + passengerCarpoolSteps[0], + driverCarpoolSteps, ); - mixedWaysteps = [ - ...mixedWaysteps.slice(0, originInsertIndex), - passengerWaysteps[0], - ...mixedWaysteps.slice(originInsertIndex), + mixedCarpoolSteps = [ + ...mixedCarpoolSteps.slice(0, originInsertIndex), + passengerCarpoolSteps[0], + ...mixedCarpoolSteps.slice(originInsertIndex), ]; const destinationInsertIndex: number = this._insertIndex( - passengerWaysteps[passengerWaysteps.length - 1], - driverWaysteps, + passengerCarpoolSteps[passengerCarpoolSteps.length - 1], + driverCarpoolSteps, ) + 1; - mixedWaysteps = [ - ...mixedWaysteps.slice(0, destinationInsertIndex), - passengerWaysteps[passengerWaysteps.length - 1], - ...mixedWaysteps.slice(destinationInsertIndex), + mixedCarpoolSteps = [ + ...mixedCarpoolSteps.slice(0, destinationInsertIndex), + passengerCarpoolSteps[passengerCarpoolSteps.length - 1], + ...mixedCarpoolSteps.slice(destinationInsertIndex), ]; - return mixedWaysteps; + return mixedCarpoolSteps; }; private _insertIndex = ( - targetWaystep: WayStep, - waysteps: WayStep[], + targetCarpoolStep: CarpoolStep, + carpoolSteps: CarpoolStep[], ): number => - this._closestSegmentIndex(targetWaystep, this._segments(waysteps)) + 1; + this._closestSegmentIndex(targetCarpoolStep, this._segments(carpoolSteps)) + + 1; - private _segments = (waysteps: WayStep[]): WayStep[][] => { - const segments: WayStep[][] = []; - waysteps.forEach((waystep: WayStep, index: number) => { - if (index < waysteps.length - 1) - segments.push([waystep, waysteps[index + 1]]); + private _segments = (carpoolSteps: CarpoolStep[]): CarpoolStep[][] => { + const segments: CarpoolStep[][] = []; + carpoolSteps.forEach((carpoolStep: CarpoolStep, index: number) => { + if (index < carpoolSteps.length - 1) + segments.push([carpoolStep, carpoolSteps[index + 1]]); }); return segments; }; private _closestSegmentIndex = ( - waystep: WayStep, - segments: WayStep[][], + carpoolStep: CarpoolStep, + segments: CarpoolStep[][], ): number => { const distances: Map = new Map(); - segments.forEach((segment: WayStep[], index: number) => { - distances.set(index, this._distanceToSegment(waystep, segment)); + segments.forEach((segment: CarpoolStep[], index: number) => { + distances.set(index, this._distanceToSegment(carpoolStep, segment)); }); const sortedDistances: Map = new Map( [...distances.entries()].sort((a, b) => a[1] - b[1]), @@ -158,30 +169,33 @@ export class CarpoolPathCreator { return closestSegmentIndex; }; - private _distanceToSegment = (waystep: WayStep, segment: WayStep[]): number => + private _distanceToSegment = ( + carpoolStep: CarpoolStep, + segment: CarpoolStep[], + ): number => parseFloat( - Math.sqrt(this._distanceToSegmentSquared(waystep, segment)).toFixed( + Math.sqrt(this._distanceToSegmentSquared(carpoolStep, segment)).toFixed( this.PRECISION, ), ); private _distanceToSegmentSquared = ( - waystep: WayStep, - segment: WayStep[], + carpoolStep: CarpoolStep, + segment: CarpoolStep[], ): number => { const length2: number = this._distanceSquared( segment[0].point, segment[1].point, ); if (length2 == 0) - return this._distanceSquared(waystep.point, segment[0].point); + return this._distanceSquared(carpoolStep.point, segment[0].point); const length: number = Math.max( 0, Math.min( 1, - ((waystep.point.lon - segment[0].point.lon) * + ((carpoolStep.point.lon - segment[0].point.lon) * (segment[1].point.lon - segment[0].point.lon) + - (waystep.point.lat - segment[0].point.lat) * + (carpoolStep.point.lat - segment[0].point.lat) * (segment[1].point.lat - segment[0].point.lat)) / length2, ), @@ -194,7 +208,7 @@ export class CarpoolPathCreator { segment[0].point.lat + length * (segment[1].point.lat - segment[0].point.lat), }); - return this._distanceSquared(waystep.point, newPoint); + return this._distanceSquared(carpoolStep.point, newPoint); }; private _distanceSquared = (point1: Point, point2: Point): number => @@ -213,29 +227,31 @@ export class CarpoolPathCreator { : Target.INTERMEDIATE; /** - * Consolidate waysteps by removing duplicate actors (eg. driver with neutral and start or finish target) + * Consolidate carpoolSteps by removing duplicate actors (eg. driver with neutral and start or finish target) */ - private _consolidate = (waysteps: WayStep[]): WayStep[] => { + private _consolidate = (carpoolSteps: CarpoolStep[]): CarpoolStep[] => { const uniquePoints: Point[] = []; - waysteps.forEach((waystep: WayStep) => { + carpoolSteps.forEach((carpoolStep: CarpoolStep) => { if ( - uniquePoints.find((point: Point) => point.isSame(waystep.point)) === + uniquePoints.find((point: Point) => point.isSame(carpoolStep.point)) === undefined ) uniquePoints.push( new Point({ - lon: waystep.point.lon, - lat: waystep.point.lat, + lon: carpoolStep.point.lon, + lat: carpoolStep.point.lat, }), ); }); return uniquePoints.map( (point: Point) => - new WayStep({ + new CarpoolStep({ point, - actors: waysteps - .filter((waystep: WayStep) => waystep.point.isSame(point)) - .map((waystep: WayStep) => waystep.actors) + actors: carpoolSteps + .filter((carpoolStep: CarpoolStep) => + carpoolStep.point.isSame(point), + ) + .map((carpoolStep: CarpoolStep) => carpoolStep.actors) .flat(), }), ); diff --git a/src/modules/ad/core/domain/path-creator.service.ts b/src/modules/ad/core/domain/path-creator.service.ts index 36114b6..ed34c0c 100644 --- a/src/modules/ad/core/domain/path-creator.service.ts +++ b/src/modules/ad/core/domain/path-creator.service.ts @@ -1,7 +1,7 @@ -import { Route } from '@modules/geography/core/domain/route.types'; import { Role } from './ad.types'; import { Point } from './value-objects/point.value-object'; import { PathCreatorException } from './match.errors'; +import { Route } from '../application/types/route.type'; export class PathCreator { constructor( diff --git a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts new file mode 100644 index 0000000..100532c --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts @@ -0,0 +1,51 @@ +import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; +import { Role } from '../ad.types'; +import { Target } from '../candidate.types'; +import { ActorProps } from './actor.value-object'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface ActorTimeProps extends ActorProps { + time: string; + minTime: string; + maxTime: string; +} + +export class ActorTime extends ValueObject { + get time(): string { + return this.props.time; + } + + get minTime(): string { + return this.props.minTime; + } + + get maxTime(): string { + return this.props.maxTime; + } + get role(): Role { + return this.props.role; + } + + get target(): Target { + return this.props.target; + } + + protected validate(props: ActorTimeProps): void { + this._validateTime(props.time, 'time'); + this._validateTime(props.minTime, 'minTime'); + this._validateTime(props.maxTime, 'maxTime'); + } + + private _validateTime(time: string, property: string): void { + if (time.split(':').length != 2) + throw new ArgumentInvalidException(`${property} is invalid`); + if (parseInt(time.split(':')[0]) < 0 || parseInt(time.split(':')[0]) > 23) + throw new ArgumentInvalidException(`${property} is invalid`); + if (parseInt(time.split(':')[1]) < 0 || parseInt(time.split(':')[1]) > 59) + throw new ArgumentInvalidException(`${property} is invalid`); + } +} diff --git a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts b/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts similarity index 78% rename from src/modules/ad/core/domain/value-objects/waystep.value-object.ts rename to src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts index bfc1f52..95382cf 100644 --- a/src/modules/ad/core/domain/value-objects/waystep.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts @@ -11,12 +11,12 @@ import { Point } from './point.value-object'; * other Value Objects inside if needed. * */ -export interface WayStepProps { +export interface CarpoolStepProps { point: Point; actors: Actor[]; } -export class WayStep extends ValueObject { +export class CarpoolStep extends ValueObject { get point(): Point { return this.props.point; } @@ -25,7 +25,7 @@ export class WayStep extends ValueObject { return this.props.actors; } - protected validate(props: WayStepProps): void { + protected validate(props: CarpoolStepProps): void { if (props.actors.length <= 0) throw new ArgumentOutOfRangeException('at least one actor is required'); if ( @@ -33,7 +33,7 @@ export class WayStep extends ValueObject { 1 ) throw new ArgumentOutOfRangeException( - 'a waystep can contain only one driver', + 'a carpoolStep can contain only one driver', ); } } diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts new file mode 100644 index 0000000..44bd626 --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -0,0 +1,42 @@ +import { + ArgumentOutOfRangeException, + ValueObject, +} from '@mobicoop/ddd-library'; +import { ScheduleItemProps } from './schedule-item.value-object'; +import { ActorTime } from './actor-time.value-object'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface JourneyProps extends ScheduleItemProps { + firstDate: Date; + lastDate: Date; + actorTimes: ActorTime[]; +} + +export class Journey extends ValueObject { + get firstDate(): Date { + return this.props.firstDate; + } + + get lastDate(): Date { + return this.props.lastDate; + } + + get actorTimes(): ActorTime[] { + return this.props.actorTimes; + } + + protected validate(props: JourneyProps): void { + if (props.firstDate > props.lastDate) + throw new ArgumentOutOfRangeException( + 'firstDate must be before lastDate', + ); + if (props.actorTimes.length <= 0) + throw new ArgumentOutOfRangeException( + 'at least one actorTime is required', + ); + } +} diff --git a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts index 97fb87f..eb32016 100644 --- a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts @@ -28,7 +28,6 @@ export class ScheduleItem extends ValueObject { return this.props.margin; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars protected validate(props: ScheduleItemProps): void { if (props.day < 0 || props.day > 6) throw new ArgumentOutOfRangeException('day must be between 0 and 6'); diff --git a/src/modules/ad/core/domain/value-objects/step.value-object.ts b/src/modules/ad/core/domain/value-objects/step.value-object.ts new file mode 100644 index 0000000..0c6d8df --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/step.value-object.ts @@ -0,0 +1,41 @@ +import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; +import { PointProps } from './point.value-object'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface StepProps extends PointProps { + duration: number; + distance?: number; +} + +export class Step extends ValueObject { + get duration(): number { + return this.props.duration; + } + + get distance(): number | undefined { + return this.props.distance; + } + + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; + } + + protected validate(props: StepProps): void { + if (props.duration < 0) + throw new ArgumentInvalidException( + 'duration must be greater than or equal to 0', + ); + if (props.distance !== undefined && props.distance < 0) + throw new ArgumentInvalidException( + 'distance must be greater than or equal to 0', + ); + } +} diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index 0c49df7..b9a8294 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -1,7 +1,7 @@ import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; -import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; +import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object'; const waypoint1: Point = new Point({ lat: 0, @@ -34,71 +34,71 @@ describe('Carpool Path Creator Service', () => { [waypoint1, waypoint6], [waypoint2, waypoint5], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(4); - expect(waysteps[0].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(4); + expect(carpoolSteps[0].actors.length).toBe(1); }); it('should create a simple carpool path with same destination for driver and passenger', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint2, waypoint6], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(3); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(2); - expect(waysteps[2].actors.length).toBe(2); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(3); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].actors.length).toBe(2); + expect(carpoolSteps[2].actors.length).toBe(2); }); it('should create a simple carpool path with same waypoints for driver and passenger', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint1, waypoint6], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(2); - expect(waysteps[0].actors.length).toBe(2); - expect(waysteps[1].actors.length).toBe(2); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(2); + expect(carpoolSteps[0].actors.length).toBe(2); + expect(carpoolSteps[1].actors.length).toBe(2); }); it('should create a complex carpool path with 3 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint6], [waypoint2, waypoint5], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(5); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(2); - expect(waysteps[2].actors.length).toBe(1); - expect(waysteps[3].actors.length).toBe(2); - expect(waysteps[4].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(5); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].actors.length).toBe(2); + expect(carpoolSteps[2].actors.length).toBe(1); + expect(carpoolSteps[3].actors.length).toBe(2); + expect(carpoolSteps[4].actors.length).toBe(1); }); it('should create a complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint4, waypoint6], [waypoint2, waypoint5], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(6); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(2); - expect(waysteps[2].actors.length).toBe(1); - expect(waysteps[3].actors.length).toBe(1); - expect(waysteps[4].actors.length).toBe(2); - expect(waysteps[5].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(6); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].actors.length).toBe(2); + expect(carpoolSteps[2].actors.length).toBe(1); + expect(carpoolSteps[3].actors.length).toBe(1); + expect(carpoolSteps[4].actors.length).toBe(2); + expect(carpoolSteps[5].actors.length).toBe(1); }); it('should create a alternate complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint2, waypoint5, waypoint6], [waypoint3, waypoint4], ); - const waysteps: WayStep[] = carpoolPathCreator.carpoolPath(); - expect(waysteps).toHaveLength(6); - expect(waysteps[0].actors.length).toBe(1); - expect(waysteps[1].actors.length).toBe(1); - expect(waysteps[2].actors.length).toBe(2); - expect(waysteps[3].actors.length).toBe(2); - expect(waysteps[4].actors.length).toBe(1); - expect(waysteps[5].actors.length).toBe(1); + const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); + expect(carpoolSteps).toHaveLength(6); + expect(carpoolSteps[0].actors.length).toBe(1); + expect(carpoolSteps[1].actors.length).toBe(1); + expect(carpoolSteps[2].actors.length).toBe(2); + expect(carpoolSteps[3].actors.length).toBe(2); + expect(carpoolSteps[4].actors.length).toBe(1); + expect(carpoolSteps[5].actors.length).toBe(1); }); it('should throw an exception if less than 2 driver waypoints are given', () => { try { diff --git a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts b/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts similarity index 76% rename from src/modules/ad/tests/unit/core/waystep.value-object.spec.ts rename to src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts index c71f530..cf65a5c 100644 --- a/src/modules/ad/tests/unit/core/waystep.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts @@ -3,11 +3,11 @@ import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; -import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; +import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object'; -describe('WayStep value object', () => { - it('should create a waystep value object', () => { - const wayStepVO = new WayStep({ +describe('CarpoolStep value object', () => { + it('should create a carpoolStep value object', () => { + const carpoolStepVO = new CarpoolStep({ point: new Point({ lat: 48.689445, lon: 6.17651, @@ -23,13 +23,13 @@ describe('WayStep value object', () => { }), ], }); - expect(wayStepVO.point.lon).toBe(6.17651); - expect(wayStepVO.point.lat).toBe(48.689445); - expect(wayStepVO.actors).toHaveLength(2); + expect(carpoolStepVO.point.lon).toBe(6.17651); + expect(carpoolStepVO.point.lat).toBe(48.689445); + expect(carpoolStepVO.actors).toHaveLength(2); }); it('should throw an exception if actors is empty', () => { try { - new WayStep({ + new CarpoolStep({ point: new Point({ lat: 48.689445, lon: 6.17651, @@ -42,7 +42,7 @@ describe('WayStep value object', () => { }); it('should throw an exception if actors contains more than one driver', () => { try { - new WayStep({ + new CarpoolStep({ point: new Point({ lat: 48.689445, lon: 6.17651, diff --git a/src/modules/ad/tests/unit/core/journey.completer.spec.ts b/src/modules/ad/tests/unit/core/journey.completer.spec.ts new file mode 100644 index 0000000..5adbeaa --- /dev/null +++ b/src/modules/ad/tests/unit/core/journey.completer.spec.ts @@ -0,0 +1,151 @@ +import { JourneyCompleter } from '@modules/ad/core/application/queries/match/completer/journey.completer'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; +import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + { + getBasic: jest.fn().mockImplementation(() => ({ + distance: 350101, + duration: 14422, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })), + getDetailed: jest.fn().mockImplementation(() => ({ + distance: 350102, + duration: 14423, + fwdAzimuth: 273, + backAzimuth: 93, + distanceAzimuth: 336544, + points: [], + })), + }, +); + +const candidate: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, +}).setCarpoolPath([ + { + point: new Point({ + lat: 48.689445, + lon: 6.17651, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.START, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.START, + }), + ], + }, + { + point: new Point({ + lat: 48.8566, + lon: 2.3522, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.FINISH, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.FINISH, + }), + ], + }, +]); + +describe('Journey completer', () => { + it('should complete candidates with their journey', async () => { + const journeyCompleter: JourneyCompleter = new JourneyCompleter(matchQuery); + const completedCandidates: CandidateEntity[] = + await journeyCompleter.complete([candidate]); + expect(completedCandidates.length).toBe(1); + }); +}); diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index b64cdf1..80291da 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -56,12 +56,13 @@ const matchQuery = new MatchQuery( points: [], })), getDetailed: jest.fn().mockImplementation(() => ({ - distance: 350102, - duration: 14423, + distance: 350101, + duration: 14422, fwdAzimuth: 273, backAzimuth: 93, distanceAzimuth: 336544, points: [], + steps: [jest.fn(), jest.fn(), jest.fn(), jest.fn()], })), }, ); @@ -163,6 +164,6 @@ describe('Route completer', () => { const completedCandidates: CandidateEntity[] = await routeCompleter.complete([candidate]); expect(completedCandidates.length).toBe(1); - expect(completedCandidates[0].getProps().distance).toBe(350102); + expect(completedCandidates[0].getProps().steps).toHaveLength(4); }); }); diff --git a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts index 6e8ec9a..9d2bf51 100644 --- a/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/route-provider.spec.ts @@ -3,9 +3,9 @@ import { AD_GET_DETAILED_ROUTE_CONTROLLER, } from '@modules/ad/ad.di-tokens'; import { Point } from '@modules/ad/core/application/types/point.type'; +import { Route } from '@modules/ad/core/application/types/route.type'; import { RouteProvider } from '@modules/ad/infrastructure/route-provider'; import { GetRouteControllerPort } from '@modules/geography/core/application/ports/get-route-controller.port'; -import { Route } from '@modules/geography/core/domain/route.types'; import { Test, TestingModule } from '@nestjs/testing'; const originPoint: Point = { From c9c682c1fc6d77e43d895fc786fa2ad4b749d3d6 Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 20 Sep 2023 11:00:04 +0200 Subject: [PATCH 40/52] fix bad tests, fix invalid value object validations --- .../queries/match/algorithm.abstract.ts | 2 +- .../value-objects/actor-time.value-object.ts | 8 +- .../value-objects/journey.value-object.ts | 37 +++- .../domain/value-objects/step.value-object.ts | 7 +- .../unit/core/actor-time.value-object.spec.ts | 49 ++++++ .../core/carpool-path-creator.service.spec.ts | 12 +- .../core/carpool-step.value-object.spec.ts | 12 +- .../unit/core/journey.value-object.spec.ts | 165 ++++++++++++++++++ .../unit/core/path-creator.service.spec.ts | 12 +- .../unit/core/point.value-object.spec.ts | 24 +-- .../core/schedule-item.value-object.spec.ts | 24 +-- .../tests/unit/core/step.value-object.spec.ts | 76 ++++++++ .../value-objects/point.value-object.ts | 2 +- .../domain/value-objects/step.value-object.ts | 7 +- .../unit/core/point.value-object.spec.ts | 24 +-- .../tests/unit/core/step.value-object.spec.ts | 44 ++--- 16 files changed, 394 insertions(+), 111 deletions(-) create mode 100644 src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts create mode 100644 src/modules/ad/tests/unit/core/journey.value-object.spec.ts create mode 100644 src/modules/ad/tests/unit/core/step.value-object.spec.ts diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index 6915366..faeb9bc 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -20,7 +20,7 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } - console.log(JSON.stringify(this.candidates, null, 2)); + // console.log(JSON.stringify(this.candidates, null, 2)); return this.candidates.map((candidate: CandidateEntity) => MatchEntity.create({ adId: candidate.id }), ); diff --git a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts index 100532c..ec38609 100644 --- a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts @@ -1,7 +1,7 @@ import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; import { Role } from '../ad.types'; import { Target } from '../candidate.types'; -import { ActorProps } from './actor.value-object'; +import { Actor, ActorProps } from './actor.value-object'; /** Note: * Value Objects with multiple properties can contain @@ -26,6 +26,7 @@ export class ActorTime extends ValueObject { get maxTime(): string { return this.props.maxTime; } + get role(): Role { return this.props.role; } @@ -35,6 +36,11 @@ export class ActorTime extends ValueObject { } protected validate(props: ActorTimeProps): void { + // validate actor props + new Actor({ + role: props.role, + target: props.target, + }); this._validateTime(props.time, 'time'); this._validateTime(props.minTime, 'minTime'); this._validateTime(props.maxTime, 'maxTime'); diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts index 44bd626..76699d0 100644 --- a/src/modules/ad/core/domain/value-objects/journey.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -1,9 +1,11 @@ import { + ArgumentInvalidException, ArgumentOutOfRangeException, ValueObject, } from '@mobicoop/ddd-library'; -import { ScheduleItemProps } from './schedule-item.value-object'; +import { ScheduleItem, ScheduleItemProps } from './schedule-item.value-object'; import { ActorTime } from './actor-time.value-object'; +import { Actor } from './actor.value-object'; /** Note: * Value Objects with multiple properties can contain @@ -29,14 +31,37 @@ export class Journey extends ValueObject { return this.props.actorTimes; } + get day(): number { + return this.props.day; + } + + get time(): string { + return this.props.time; + } + + get margin(): number { + return this.props.margin; + } + protected validate(props: JourneyProps): void { + // validate scheduleItem props + new ScheduleItem({ + day: props.day, + time: props.time, + margin: props.margin, + }); + // validate actor times + props.actorTimes.forEach((actorTime: ActorTime) => { + new Actor({ + role: actorTime.role, + target: actorTime.target, + }); + }); if (props.firstDate > props.lastDate) + throw new ArgumentInvalidException('firstDate must be before lastDate'); + if (props.actorTimes.length < 4) throw new ArgumentOutOfRangeException( - 'firstDate must be before lastDate', - ); - if (props.actorTimes.length <= 0) - throw new ArgumentOutOfRangeException( - 'at least one actorTime is required', + 'at least 4 actorTimes are required', ); } } diff --git a/src/modules/ad/core/domain/value-objects/step.value-object.ts b/src/modules/ad/core/domain/value-objects/step.value-object.ts index 0c6d8df..fbcc410 100644 --- a/src/modules/ad/core/domain/value-objects/step.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/step.value-object.ts @@ -1,5 +1,5 @@ import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; -import { PointProps } from './point.value-object'; +import { Point, PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain @@ -29,6 +29,11 @@ export class Step extends ValueObject { } protected validate(props: StepProps): void { + // validate point props + new Point({ + lon: props.lon, + lat: props.lat, + }); if (props.duration < 0) throw new ArgumentInvalidException( 'duration must be greater than or equal to 0', diff --git a/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts new file mode 100644 index 0000000..e3cdaaf --- /dev/null +++ b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts @@ -0,0 +1,49 @@ +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; + +describe('Actor time value object', () => { + it('should create an actor time value object', () => { + const actorTimeVO = new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '07:00', + minTime: '06:45', + maxTime: '07:15', + }); + expect(actorTimeVO.role).toBe(Role.DRIVER); + expect(actorTimeVO.target).toBe(Target.START); + expect(actorTimeVO.time).toBe('07:00'); + expect(actorTimeVO.minTime).toBe('06:45'); + expect(actorTimeVO.maxTime).toBe('07:15'); + }); + it('should throw an error if a time is invalid', () => { + expect(() => { + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '27:00', + minTime: '06:45', + maxTime: '07:15', + }); + }).toThrow(); + expect(() => { + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '07:00', + minTime: '06:95', + maxTime: '07:15', + }); + }).toThrow(); + expect(() => { + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '07:00', + minTime: '06:45', + maxTime: '07', + }); + }).toThrow(); + }); +}); diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index b9a8294..8fbb643 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -101,17 +101,13 @@ describe('Carpool Path Creator Service', () => { expect(carpoolSteps[5].actors.length).toBe(1); }); it('should throw an exception if less than 2 driver waypoints are given', () => { - try { + expect(() => { new CarpoolPathCreator([waypoint1], [waypoint3, waypoint4]); - } catch (e: any) { - expect(e).toBeInstanceOf(CarpoolPathCreatorException); - } + }).toThrow(CarpoolPathCreatorException); }); it('should throw an exception if less than 2 passenger waypoints are given', () => { - try { + expect(() => { new CarpoolPathCreator([waypoint1, waypoint6], [waypoint3]); - } catch (e: any) { - expect(e).toBeInstanceOf(CarpoolPathCreatorException); - } + }).toThrow(CarpoolPathCreatorException); }); }); diff --git a/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts b/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts index cf65a5c..d633079 100644 --- a/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts @@ -28,7 +28,7 @@ describe('CarpoolStep value object', () => { expect(carpoolStepVO.actors).toHaveLength(2); }); it('should throw an exception if actors is empty', () => { - try { + expect(() => { new CarpoolStep({ point: new Point({ lat: 48.689445, @@ -36,12 +36,10 @@ describe('CarpoolStep value object', () => { }), actors: [], }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if actors contains more than one driver', () => { - try { + expect(() => { new CarpoolStep({ point: new Point({ lat: 48.689445, @@ -58,8 +56,6 @@ describe('CarpoolStep value object', () => { }), ], }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); }); diff --git a/src/modules/ad/tests/unit/core/journey.value-object.spec.ts b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts new file mode 100644 index 0000000..5f8ec1e --- /dev/null +++ b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts @@ -0,0 +1,165 @@ +import { + ArgumentInvalidException, + ArgumentOutOfRangeException, +} from '@mobicoop/ddd-library'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; + +describe('Journey value object', () => { + it('should create a journey value object', () => { + const journeyVO = new Journey({ + firstDate: new Date('2023-09-20'), + lastDate: new Date('2024-09-20'), + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '07:00', + minTime: '06:45', + maxTime: '07:15', + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + time: '07:10', + minTime: '06:55', + maxTime: '07:25', + }), + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + time: '08:30', + minTime: '08:15', + maxTime: '08:45', + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + time: '08:40', + minTime: '08:25', + maxTime: '08:55', + }), + ], + day: 0, + time: '07:00', + margin: 900, + }); + expect(journeyVO.day).toBe(0); + expect(journeyVO.time).toBe('07:00'); + expect(journeyVO.margin).toBe(900); + expect(journeyVO.actorTimes).toHaveLength(4); + expect(journeyVO.firstDate.getDate()).toBe(20); + expect(journeyVO.lastDate.getMonth()).toBe(8); + }); + it('should throw an exception if day is invalid', () => { + expect(() => { + new Journey({ + firstDate: new Date('2023-09-20'), + lastDate: new Date('2024-09-20'), + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '07:00', + minTime: '06:45', + maxTime: '07:15', + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + time: '07:10', + minTime: '06:55', + maxTime: '07:25', + }), + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + time: '08:30', + minTime: '08:15', + maxTime: '08:45', + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + time: '08:40', + minTime: '08:25', + maxTime: '08:55', + }), + ], + day: 7, + time: '07:00', + margin: 900, + }); + }).toThrow(ArgumentOutOfRangeException); + }); + it('should throw an exception if actor times is too short', () => { + expect(() => { + new Journey({ + firstDate: new Date('2023-09-20'), + lastDate: new Date('2024-09-20'), + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '07:00', + minTime: '06:45', + maxTime: '07:15', + }), + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + time: '08:30', + minTime: '08:15', + maxTime: '08:45', + }), + ], + day: 0, + time: '07:00', + margin: 900, + }); + }).toThrow(ArgumentOutOfRangeException); + }); + it('should throw an exception if dates are invalid', () => { + expect(() => { + new Journey({ + firstDate: new Date('2023-09-20'), + lastDate: new Date('2023-09-19'), + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + time: '07:00', + minTime: '06:45', + maxTime: '07:15', + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + time: '07:10', + minTime: '06:55', + maxTime: '07:25', + }), + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + time: '08:30', + minTime: '08:15', + maxTime: '08:45', + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + time: '08:40', + minTime: '08:25', + maxTime: '08:55', + }), + ], + day: 0, + time: '07:00', + margin: 900, + }); + }).toThrow(ArgumentInvalidException); + }); +}); diff --git a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts index 7c7c5d0..895627d 100644 --- a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts @@ -70,20 +70,16 @@ describe('Path Creator Service', () => { ).toHaveLength(2); }); it('should throw an exception if a role is not given', () => { - try { + expect(() => { new PathCreator( [], [originWaypoint, intermediateWaypoint, destinationWaypoint], ); - } catch (e: any) { - expect(e).toBeInstanceOf(PathCreatorException); - } + }).toThrow(PathCreatorException); }); it('should throw an exception if less than 2 waypoints are given', () => { - try { + expect(() => { new PathCreator([Role.DRIVER], [originWaypoint]); - } catch (e: any) { - expect(e).toBeInstanceOf(PathCreatorException); - } + }).toThrow(PathCreatorException); }); }); diff --git a/src/modules/ad/tests/unit/core/point.value-object.spec.ts b/src/modules/ad/tests/unit/core/point.value-object.spec.ts index fb9943b..7f1fda0 100644 --- a/src/modules/ad/tests/unit/core/point.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/point.value-object.spec.ts @@ -27,39 +27,31 @@ describe('Point value object', () => { expect(pointVO.isSame(differentPointVO)).toBeFalsy(); }); it('should throw an exception if longitude is invalid', () => { - try { + expect(() => { new Point({ lat: 48.689445, lon: 186.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { + }).toThrow(ArgumentOutOfRangeException); + expect(() => { new Point({ lat: 48.689445, lon: -186.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if latitude is invalid', () => { - try { + expect(() => { new Point({ lat: 148.689445, lon: 6.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { + }).toThrow(ArgumentOutOfRangeException); + expect(() => { new Point({ lat: -148.689445, lon: 6.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); }); diff --git a/src/modules/ad/tests/unit/core/schedule-item.value-object.spec.ts b/src/modules/ad/tests/unit/core/schedule-item.value-object.spec.ts index 65bacc9..87e6041 100644 --- a/src/modules/ad/tests/unit/core/schedule-item.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/schedule-item.value-object.spec.ts @@ -16,47 +16,39 @@ describe('Schedule item value object', () => { expect(scheduleItemVO.margin).toBe(900); }); it('should throw an exception if day is invalid', () => { - try { + expect(() => { new ScheduleItem({ day: 7, time: '07:00', margin: 900, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if time is invalid', () => { - try { + expect(() => { new ScheduleItem({ day: 0, time: '07,00', margin: 900, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentInvalidException); - } + }).toThrow(ArgumentInvalidException); }); it('should throw an exception if the hour of the time is invalid', () => { - try { + expect(() => { new ScheduleItem({ day: 0, time: '25:00', margin: 900, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentInvalidException); - } + }).toThrow(ArgumentInvalidException); }); it('should throw an exception if the minutes of the time are invalid', () => { - try { + expect(() => { new ScheduleItem({ day: 0, time: '07:63', margin: 900, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentInvalidException); - } + }).toThrow(ArgumentInvalidException); }); }); diff --git a/src/modules/ad/tests/unit/core/step.value-object.spec.ts b/src/modules/ad/tests/unit/core/step.value-object.spec.ts new file mode 100644 index 0000000..cadf89e --- /dev/null +++ b/src/modules/ad/tests/unit/core/step.value-object.spec.ts @@ -0,0 +1,76 @@ +import { + ArgumentInvalidException, + ArgumentOutOfRangeException, +} from '@mobicoop/ddd-library'; +import { Step } from '@modules/ad/core/domain/value-objects/step.value-object'; + +describe('Step value object', () => { + it('should create a step value object', () => { + const stepVO = new Step({ + lat: 48.689445, + lon: 6.17651, + duration: 150, + distance: 12000, + }); + expect(stepVO.duration).toBe(150); + expect(stepVO.distance).toBe(12000); + expect(stepVO.lat).toBe(48.689445); + expect(stepVO.lon).toBe(6.17651); + }); + it('should throw an exception if longitude is invalid', () => { + expect(() => { + new Step({ + lat: 48.689445, + lon: 186.17651, + duration: 150, + distance: 12000, + }); + }).toThrow(ArgumentOutOfRangeException); + expect(() => { + new Step({ + lat: 48.689445, + lon: -186.17651, + duration: 150, + distance: 12000, + }); + }).toThrow(ArgumentOutOfRangeException); + }); + it('should throw an exception if latitude is invalid', () => { + expect(() => { + new Step({ + lat: 248.689445, + lon: 6.17651, + duration: 150, + distance: 12000, + }); + }).toThrow(ArgumentOutOfRangeException); + expect(() => { + new Step({ + lat: -148.689445, + lon: 6.17651, + duration: 150, + distance: 12000, + }); + }).toThrow(ArgumentOutOfRangeException); + }); + it('should throw an exception if distance is invalid', () => { + expect(() => { + new Step({ + lat: 48.689445, + lon: 6.17651, + duration: 150, + distance: -12000, + }); + }).toThrow(ArgumentInvalidException); + }); + it('should throw an exception if duration is invalid', () => { + expect(() => { + new Step({ + lat: 48.689445, + lon: 6.17651, + duration: -150, + distance: 12000, + }); + }).toThrow(ArgumentInvalidException); + }); +}); diff --git a/src/modules/geography/core/domain/value-objects/point.value-object.ts b/src/modules/geography/core/domain/value-objects/point.value-object.ts index 2047ead..48e6564 100644 --- a/src/modules/geography/core/domain/value-objects/point.value-object.ts +++ b/src/modules/geography/core/domain/value-objects/point.value-object.ts @@ -22,7 +22,7 @@ export class Point extends ValueObject { return this.props.lat; } - protected validate(props: PointProps): void { + validate(props: PointProps): void { if (props.lon > 180 || props.lon < -180) throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); if (props.lat > 90 || props.lat < -90) diff --git a/src/modules/geography/core/domain/value-objects/step.value-object.ts b/src/modules/geography/core/domain/value-objects/step.value-object.ts index 0c6d8df..fbcc410 100644 --- a/src/modules/geography/core/domain/value-objects/step.value-object.ts +++ b/src/modules/geography/core/domain/value-objects/step.value-object.ts @@ -1,5 +1,5 @@ import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; -import { PointProps } from './point.value-object'; +import { Point, PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain @@ -29,6 +29,11 @@ export class Step extends ValueObject { } protected validate(props: StepProps): void { + // validate point props + new Point({ + lon: props.lon, + lat: props.lat, + }); if (props.duration < 0) throw new ArgumentInvalidException( 'duration must be greater than or equal to 0', diff --git a/src/modules/geography/tests/unit/core/point.value-object.spec.ts b/src/modules/geography/tests/unit/core/point.value-object.spec.ts index 8fc5573..bf11cd7 100644 --- a/src/modules/geography/tests/unit/core/point.value-object.spec.ts +++ b/src/modules/geography/tests/unit/core/point.value-object.spec.ts @@ -11,39 +11,31 @@ describe('Point value object', () => { expect(pointVO.lon).toBe(6.17651); }); it('should throw an exception if longitude is invalid', () => { - try { + expect(() => { new Point({ lat: 48.689445, lon: 186.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { + }).toThrow(ArgumentOutOfRangeException); + expect(() => { new Point({ lat: 48.689445, lon: -186.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if latitude is invalid', () => { - try { + expect(() => { new Point({ lat: 148.689445, lon: 6.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { + }).toThrow(ArgumentOutOfRangeException); + expect(() => { new Point({ lat: -148.689445, lon: 6.17651, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); }); diff --git a/src/modules/geography/tests/unit/core/step.value-object.spec.ts b/src/modules/geography/tests/unit/core/step.value-object.spec.ts index 6811c97..ed84ad8 100644 --- a/src/modules/geography/tests/unit/core/step.value-object.spec.ts +++ b/src/modules/geography/tests/unit/core/step.value-object.spec.ts @@ -18,71 +18,59 @@ describe('Step value object', () => { expect(stepVO.lon).toBe(6.17651); }); it('should throw an exception if longitude is invalid', () => { - try { + expect(() => { new Step({ lat: 48.689445, lon: 186.17651, duration: 150, distance: 12000, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { + }).toThrow(ArgumentOutOfRangeException); + expect(() => { new Step({ - lon: 48.689445, - lat: -186.17651, + lat: 48.689445, + lon: -186.17651, duration: 150, distance: 12000, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if latitude is invalid', () => { - try { + expect(() => { new Step({ lat: 248.689445, lon: 6.17651, duration: 150, distance: 12000, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } - try { + }).toThrow(ArgumentOutOfRangeException); + expect(() => { new Step({ - lon: -148.689445, - lat: 6.17651, + lat: -148.689445, + lon: 6.17651, duration: 150, distance: 12000, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentOutOfRangeException); - } + }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if distance is invalid', () => { - try { + expect(() => { new Step({ lat: 48.689445, lon: 6.17651, duration: 150, distance: -12000, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentInvalidException); - } + }).toThrow(ArgumentInvalidException); }); it('should throw an exception if duration is invalid', () => { - try { + expect(() => { new Step({ lat: 48.689445, lon: 6.17651, duration: -150, distance: 12000, }); - } catch (e: any) { - expect(e).toBeInstanceOf(ArgumentInvalidException); - } + }).toThrow(ArgumentInvalidException); }); }); From dfc8dbcc519eea70c2a1aecfc5103b0f59593afd Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 21 Sep 2023 12:44:39 +0200 Subject: [PATCH 41/52] journey and journey item value objects --- .../match/completer/journey.completer.ts | 4 +- .../match/passenger-oriented-algorithm.ts | 2 + .../ad/core/domain/calendar-tools.service.ts | 80 +++ .../ad/core/domain/candidate.entity.ts | 38 +- src/modules/ad/core/domain/candidate.types.ts | 3 +- .../domain/carpool-path-creator.service.ts | 6 +- .../value-objects/actor-time.value-object.ts | 81 ++- .../journey-item.value-object.ts | 51 ++ .../value-objects/journey.value-object.ts | 46 +- .../value-objects/point.value-object.ts | 3 - .../unit/core/actor-time.value-object.spec.ts | 126 ++-- .../unit/core/calendar-tools.service.spec.ts | 84 +++ .../core/journey-item.value-object.spec.ts | 45 ++ .../tests/unit/core/journey.completer.spec.ts | 5 +- .../unit/core/journey.value-object.spec.ts | 576 +++++++++++++----- .../unit/core/point.value-object.spec.ts | 4 +- 16 files changed, 907 insertions(+), 247 deletions(-) create mode 100644 src/modules/ad/core/domain/calendar-tools.service.ts create mode 100644 src/modules/ad/core/domain/value-objects/journey-item.value-object.ts create mode 100644 src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts create mode 100644 src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts diff --git a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts index ac4ae5f..b50ebf5 100644 --- a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts @@ -5,5 +5,7 @@ export class JourneyCompleter extends Completer { complete = async ( candidates: CandidateEntity[], ): Promise => - candidates.map((candidate: CandidateEntity) => candidate.createJourney()); + candidates.map((candidate: CandidateEntity) => + candidate.createJourneys(this.query.fromDate, this.query.toDate), + ); } diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index 618a5dc..6baa3b4 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -8,6 +8,7 @@ import { RouteCompleter, RouteCompleterType, } from './completer/route.completer'; +import { JourneyCompleter } from './completer/journey.completer'; export class PassengerOrientedAlgorithm extends Algorithm { constructor( @@ -21,6 +22,7 @@ export class PassengerOrientedAlgorithm extends Algorithm { new RouteCompleter(query, RouteCompleterType.BASIC), new PassengerOrientedGeoFilter(query), new RouteCompleter(query, RouteCompleterType.DETAILED), + new JourneyCompleter(query), ]; } } diff --git a/src/modules/ad/core/domain/calendar-tools.service.ts b/src/modules/ad/core/domain/calendar-tools.service.ts new file mode 100644 index 0000000..b0d2b4f --- /dev/null +++ b/src/modules/ad/core/domain/calendar-tools.service.ts @@ -0,0 +1,80 @@ +import { ExceptionBase } from '@mobicoop/ddd-library'; + +export class CalendarTools { + /** + * Returns the first date corresponding to a week day (0 based monday) + * within a date range + */ + static firstDate = ( + weekDay: number, + lowerDate: string, + higherDate: string, + ): Date => { + if (weekDay < 0 || weekDay > 6) + throw new CalendarToolsException( + new Error('weekDay must be between 0 and 6'), + ); + const lowerDateAsDate: Date = new Date(lowerDate); + const higherDateAsDate: Date = new Date(higherDate); + if (lowerDateAsDate.getDay() == weekDay) return lowerDateAsDate; + const nextDate: Date = new Date(lowerDateAsDate); + nextDate.setDate( + lowerDateAsDate.getDate() + (7 - (lowerDateAsDate.getDay() - weekDay)), + ); + if (lowerDateAsDate.getDay() < weekDay) { + nextDate.setMonth(lowerDateAsDate.getMonth()); + nextDate.setFullYear(lowerDateAsDate.getFullYear()); + nextDate.setDate( + lowerDateAsDate.getDate() + (weekDay - lowerDateAsDate.getDay()), + ); + } + if (nextDate <= higherDateAsDate) return nextDate; + throw new CalendarToolsException( + new Error('no available day for the given date range'), + ); + }; + + /** + * Returns the last date corresponding to a week day (0 based monday) + * within a date range + */ + static lastDate = ( + weekDay: number, + lowerDate: string, + higherDate: string, + ): Date => { + if (weekDay < 0 || weekDay > 6) + throw new CalendarToolsException( + new Error('weekDay must be between 0 and 6'), + ); + const lowerDateAsDate: Date = new Date(lowerDate); + const higherDateAsDate: Date = new Date(higherDate); + if (higherDateAsDate.getDay() == weekDay) return higherDateAsDate; + const previousDate: Date = new Date(higherDateAsDate); + previousDate.setDate( + higherDateAsDate.getDate() - (higherDateAsDate.getDay() - weekDay), + ); + if (higherDateAsDate.getDay() < weekDay) { + previousDate.setMonth(higherDateAsDate.getMonth()); + previousDate.setFullYear(higherDateAsDate.getFullYear()); + previousDate.setDate( + higherDateAsDate.getDate() - + (7 + (higherDateAsDate.getDay() - weekDay)), + ); + } + if (previousDate >= lowerDateAsDate) return previousDate; + throw new CalendarToolsException( + new Error('no available day for the given date range'), + ); + }; +} + +export class CalendarToolsException extends ExceptionBase { + static readonly message = 'Calendar tools error'; + + public readonly code = 'CALENDAR.TOOLS'; + + constructor(cause?: Error, metadata?: unknown) { + super(CalendarToolsException.message, cause, metadata); + } +} diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 7e4b1ad..4bd0143 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -2,6 +2,10 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { CandidateProps, CreateCandidateProps } from './candidate.types'; import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; import { StepProps } from './value-objects/step.value-object'; +import { ScheduleItem } from './value-objects/schedule-item.value-object'; +import { Journey } from './value-objects/journey.value-object'; +import { CalendarTools } from './calendar-tools.service'; +import { JourneyItem } from './value-objects/journey-item.value-object'; export class CandidateEntity extends AggregateRoot { protected readonly _id: AggregateID; @@ -30,7 +34,23 @@ export class CandidateEntity extends AggregateRoot { isDetourValid = (): boolean => this._validateDistanceDetour() && this._validateDurationDetour(); - createJourney = (): CandidateEntity => this; + createJourneys = (fromDate: string, toDate: string): CandidateEntity => { + this.props.driverJourneys = this.props.driverSchedule + .map((driverScheduleItem: ScheduleItem) => + this._createJourney(fromDate, toDate, driverScheduleItem), + ) + .filter( + (journey: Journey | undefined) => journey !== undefined, + ) as Journey[]; + this.props.passengerJourneys = this.props.passengerSchedule + .map((passengerScheduleItem: ScheduleItem) => + this._createJourney(fromDate, toDate, passengerScheduleItem), + ) + .filter( + (journey: Journey | undefined) => journey !== undefined, + ) as Journey[]; + return this; + }; private _validateDurationDetour = (): boolean => this.props.duration @@ -46,6 +66,22 @@ export class CandidateEntity extends AggregateRoot { (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) : false; + private _createJourney = ( + fromDate: string, + toDate: string, + scheduleItem: ScheduleItem, + ): Journey | undefined => + new Journey({ + day: scheduleItem.day, + firstDate: CalendarTools.firstDate(scheduleItem.day, fromDate, toDate), + lastDate: CalendarTools.lastDate(scheduleItem.day, fromDate, toDate), + journeyItems: this._createJourneyItems(scheduleItem), + }); + + private _createJourneyItems = ( + scheduleItem: ScheduleItem, + ): JourneyItem[] => []; + validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 3a68d06..07b5d27 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -18,7 +18,8 @@ export interface CandidateProps { steps?: StepProps[]; driverSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[]; - journeys?: JourneyProps[]; + driverJourneys?: JourneyProps[]; + passengerJourneys?: JourneyProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; } diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index 43f61dd..68a0a51 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -86,7 +86,7 @@ export class CarpoolPathCreator { }); if ( this.driverWaypoints.filter((driverWaypoint: Point) => - passengerWaypoint.isSame(driverWaypoint), + passengerWaypoint.equals(driverWaypoint), ).length == 0 ) { carpoolStep.actors.push( @@ -233,7 +233,7 @@ export class CarpoolPathCreator { const uniquePoints: Point[] = []; carpoolSteps.forEach((carpoolStep: CarpoolStep) => { if ( - uniquePoints.find((point: Point) => point.isSame(carpoolStep.point)) === + uniquePoints.find((point: Point) => point.equals(carpoolStep.point)) === undefined ) uniquePoints.push( @@ -249,7 +249,7 @@ export class CarpoolPathCreator { point, actors: carpoolSteps .filter((carpoolStep: CarpoolStep) => - carpoolStep.point.isSame(point), + carpoolStep.point.equals(point), ) .map((carpoolStep: CarpoolStep) => carpoolStep.actors) .flat(), diff --git a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts index ec38609..9702dcb 100644 --- a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts @@ -9,24 +9,15 @@ import { Actor, ActorProps } from './actor.value-object'; * */ export interface ActorTimeProps extends ActorProps { - time: string; - minTime: string; - maxTime: string; + firstDatetime: Date; + firstMinDatetime: Date; + firstMaxDatetime: Date; + lastDatetime: Date; + lastMinDatetime: Date; + lastMaxDatetime: Date; } export class ActorTime extends ValueObject { - get time(): string { - return this.props.time; - } - - get minTime(): string { - return this.props.minTime; - } - - get maxTime(): string { - return this.props.maxTime; - } - get role(): Role { return this.props.role; } @@ -35,23 +26,59 @@ export class ActorTime extends ValueObject { return this.props.target; } + get firstDatetime(): Date { + return this.props.firstDatetime; + } + + get firstMinDatetime(): Date { + return this.props.firstMinDatetime; + } + + get firstMaxDatetime(): Date { + return this.props.firstMaxDatetime; + } + + get lastDatetime(): Date { + return this.props.lastDatetime; + } + + get lastMinDatetime(): Date { + return this.props.lastMinDatetime; + } + + get lastMaxDatetime(): Date { + return this.props.lastMaxDatetime; + } + protected validate(props: ActorTimeProps): void { // validate actor props new Actor({ role: props.role, target: props.target, }); - this._validateTime(props.time, 'time'); - this._validateTime(props.minTime, 'minTime'); - this._validateTime(props.maxTime, 'maxTime'); - } - - private _validateTime(time: string, property: string): void { - if (time.split(':').length != 2) - throw new ArgumentInvalidException(`${property} is invalid`); - if (parseInt(time.split(':')[0]) < 0 || parseInt(time.split(':')[0]) > 23) - throw new ArgumentInvalidException(`${property} is invalid`); - if (parseInt(time.split(':')[1]) < 0 || parseInt(time.split(':')[1]) > 59) - throw new ArgumentInvalidException(`${property} is invalid`); + if (props.firstDatetime.getDay() != props.lastDatetime.getDay()) + throw new ArgumentInvalidException( + 'firstDatetime week day must be equal to lastDatetime week day', + ); + if (props.firstDatetime > props.lastDatetime) + throw new ArgumentInvalidException( + 'firstDatetime must be before or equal to lastDatetime', + ); + if (props.firstMinDatetime > props.firstDatetime) + throw new ArgumentInvalidException( + 'firstMinDatetime must be before or equal to firstDatetime', + ); + if (props.firstDatetime > props.firstMaxDatetime) + throw new ArgumentInvalidException( + 'firstDatetime must be before or equal to firstMaxDatetime', + ); + if (props.lastMinDatetime > props.lastDatetime) + throw new ArgumentInvalidException( + 'lastMinDatetime must be before or equal to lastDatetime', + ); + if (props.lastDatetime > props.lastMaxDatetime) + throw new ArgumentInvalidException( + 'lastDatetime must be before or equal to lastMaxDatetime', + ); } } diff --git a/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts b/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts new file mode 100644 index 0000000..4f1a9d4 --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts @@ -0,0 +1,51 @@ +import { + ArgumentOutOfRangeException, + ValueObject, +} from '@mobicoop/ddd-library'; +import { ActorTime } from './actor-time.value-object'; +import { Step, StepProps } from './step.value-object'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface JourneyItemProps extends StepProps { + actorTimes: ActorTime[]; +} + +export class JourneyItem extends ValueObject { + get duration(): number { + return this.props.duration; + } + + get distance(): number | undefined { + return this.props.distance; + } + + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; + } + + get actorTimes(): ActorTime[] { + return this.props.actorTimes; + } + + protected validate(props: JourneyItemProps): void { + // validate step props + new Step({ + lon: props.lon, + lat: props.lat, + distance: props.distance, + duration: props.duration, + }); + if (props.actorTimes.length == 0) + throw new ArgumentOutOfRangeException( + 'at least one actorTime is required', + ); + } +} diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts index 76699d0..fb7f421 100644 --- a/src/modules/ad/core/domain/value-objects/journey.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -3,19 +3,18 @@ import { ArgumentOutOfRangeException, ValueObject, } from '@mobicoop/ddd-library'; -import { ScheduleItem, ScheduleItemProps } from './schedule-item.value-object'; -import { ActorTime } from './actor-time.value-object'; -import { Actor } from './actor.value-object'; +import { JourneyItem } from './journey-item.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface JourneyProps extends ScheduleItemProps { +export interface JourneyProps { firstDate: Date; lastDate: Date; - actorTimes: ActorTime[]; + day: number; + journeyItems: JourneyItem[]; } export class Journey extends ValueObject { @@ -27,41 +26,26 @@ export class Journey extends ValueObject { return this.props.lastDate; } - get actorTimes(): ActorTime[] { - return this.props.actorTimes; - } - get day(): number { return this.props.day; } - get time(): string { - return this.props.time; - } - - get margin(): number { - return this.props.margin; + get journeyItems(): JourneyItem[] { + return this.props.journeyItems; } protected validate(props: JourneyProps): void { - // validate scheduleItem props - new ScheduleItem({ - day: props.day, - time: props.time, - margin: props.margin, - }); - // validate actor times - props.actorTimes.forEach((actorTime: ActorTime) => { - new Actor({ - role: actorTime.role, - target: actorTime.target, - }); - }); + if (props.day < 0 || props.day > 6) + throw new ArgumentOutOfRangeException('day must be between 0 and 6'); + if (props.firstDate.getDay() != props.lastDate.getDay()) + throw new ArgumentInvalidException( + 'firstDate week day must be equal to lastDate week day', + ); if (props.firstDate > props.lastDate) throw new ArgumentInvalidException('firstDate must be before lastDate'); - if (props.actorTimes.length < 4) - throw new ArgumentOutOfRangeException( - 'at least 4 actorTimes are required', + if (props.journeyItems.length < 2) + throw new ArgumentInvalidException( + 'at least 2 journey items are required', ); } } diff --git a/src/modules/ad/core/domain/value-objects/point.value-object.ts b/src/modules/ad/core/domain/value-objects/point.value-object.ts index 54a2677..2047ead 100644 --- a/src/modules/ad/core/domain/value-objects/point.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/point.value-object.ts @@ -22,9 +22,6 @@ export class Point extends ValueObject { return this.props.lat; } - isSame = (point: this): boolean => - point.lon == this.lon && point.lat == this.lat; - protected validate(props: PointProps): void { if (props.lon > 180 || props.lon < -180) throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); diff --git a/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts index e3cdaaf..3d7ed4e 100644 --- a/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts @@ -1,3 +1,4 @@ +import { ArgumentInvalidException } from '@mobicoop/ddd-library'; import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; @@ -7,43 +8,100 @@ describe('Actor time value object', () => { const actorTimeVO = new ActorTime({ role: Role.DRIVER, target: Target.START, - time: '07:00', - minTime: '06:45', - maxTime: '07:15', + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), }); expect(actorTimeVO.role).toBe(Role.DRIVER); expect(actorTimeVO.target).toBe(Target.START); - expect(actorTimeVO.time).toBe('07:00'); - expect(actorTimeVO.minTime).toBe('06:45'); - expect(actorTimeVO.maxTime).toBe('07:15'); + expect(actorTimeVO.firstDatetime.getHours()).toBe(7); + expect(actorTimeVO.firstMinDatetime.getMinutes()).toBe(45); + expect(actorTimeVO.firstMaxDatetime.getMinutes()).toBe(15); + expect(actorTimeVO.lastDatetime.getHours()).toBe(7); + expect(actorTimeVO.lastMinDatetime.getMinutes()).toBe(45); + expect(actorTimeVO.lastMaxDatetime.getMinutes()).toBe(15); }); - it('should throw an error if a time is invalid', () => { - expect(() => { - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - time: '27:00', - minTime: '06:45', - maxTime: '07:15', - }); - }).toThrow(); - expect(() => { - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - time: '07:00', - minTime: '06:95', - maxTime: '07:15', - }); - }).toThrow(); - expect(() => { - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - time: '07:00', - minTime: '06:45', - maxTime: '07', - }); - }).toThrow(); + it('should throw an error if dates are inconsistent', () => { + expect( + () => + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 07:05'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ).toThrow(ArgumentInvalidException); + expect( + () => + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 06:55'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ).toThrow(ArgumentInvalidException); + expect( + () => + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 07:05'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ).toThrow(ArgumentInvalidException); + expect( + () => + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 06:35'), + }), + ).toThrow(ArgumentInvalidException); + expect( + () => + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2024-08-30 07:00'), + firstMinDatetime: new Date('2024-08-30 06:45'), + firstMaxDatetime: new Date('2024-08-30 07:15'), + lastDatetime: new Date('2023-09-01 07:00'), + lastMinDatetime: new Date('2023-09-01 06:45'), + lastMaxDatetime: new Date('2023-09-01 07:15'), + }), + ).toThrow(ArgumentInvalidException); + expect( + () => + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-31 07:00'), + lastMinDatetime: new Date('2024-08-31 06:45'), + lastMaxDatetime: new Date('2024-08-31 06:35'), + }), + ).toThrow(ArgumentInvalidException); }); }); diff --git a/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts new file mode 100644 index 0000000..a31ff87 --- /dev/null +++ b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts @@ -0,0 +1,84 @@ +import { + CalendarTools, + CalendarToolsException, +} from '@modules/ad/core/domain/calendar-tools.service'; + +describe('Calendar tools service', () => { + describe('First date', () => { + it('should return the first date for a given week day within a date range', () => { + const firstDate: Date = CalendarTools.firstDate( + 1, + '2023-08-31', + '2023-09-07', + ); + expect(firstDate.getDay()).toBe(1); + expect(firstDate.getDate()).toBe(4); + expect(firstDate.getMonth()).toBe(8); + const secondDate: Date = CalendarTools.firstDate( + 5, + '2023-08-31', + '2023-09-07', + ); + expect(secondDate.getDay()).toBe(5); + expect(secondDate.getDate()).toBe(1); + expect(secondDate.getMonth()).toBe(8); + const thirdDate: Date = CalendarTools.firstDate( + 4, + '2023-08-31', + '2023-09-07', + ); + expect(thirdDate.getDay()).toBe(4); + expect(thirdDate.getDate()).toBe(31); + expect(thirdDate.getMonth()).toBe(7); + }); + it('should throw an exception if a given week day is not within a date range', () => { + expect(() => { + CalendarTools.firstDate(1, '2023-09-05', '2023-09-07'); + }).toThrow(CalendarToolsException); + }); + it('should throw an exception if a given week day is invalid', () => { + expect(() => { + CalendarTools.firstDate(8, '2023-09-05', '2023-09-07'); + }).toThrow(CalendarToolsException); + }); + }); + + describe('Second date', () => { + it('should return the last date for a given week day within a date range', () => { + const firstDate: Date = CalendarTools.lastDate( + 0, + '2023-09-30', + '2024-09-30', + ); + expect(firstDate.getDay()).toBe(0); + expect(firstDate.getDate()).toBe(29); + expect(firstDate.getMonth()).toBe(8); + const secondDate: Date = CalendarTools.lastDate( + 5, + '2023-09-30', + '2024-09-30', + ); + expect(secondDate.getDay()).toBe(5); + expect(secondDate.getDate()).toBe(27); + expect(secondDate.getMonth()).toBe(8); + const thirdDate: Date = CalendarTools.lastDate( + 1, + '2023-09-30', + '2024-09-30', + ); + expect(thirdDate.getDay()).toBe(1); + expect(thirdDate.getDate()).toBe(30); + expect(thirdDate.getMonth()).toBe(8); + }); + it('should throw an exception if a given week day is not within a date range', () => { + expect(() => { + CalendarTools.lastDate(2, '2024-09-27', '2024-09-30'); + }).toThrow(CalendarToolsException); + }); + it('should throw an exception if a given week day is invalid', () => { + expect(() => { + CalendarTools.lastDate(8, '2023-09-30', '2024-09-30'); + }).toThrow(CalendarToolsException); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts b/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts new file mode 100644 index 0000000..c4461c1 --- /dev/null +++ b/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts @@ -0,0 +1,45 @@ +import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; + +describe('Journey item value object', () => { + it('should create a journey item value object', () => { + const journeyItemVO: JourneyItem = new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 1545, + distance: 48754, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }); + expect(journeyItemVO.duration).toBe(1545); + expect(journeyItemVO.distance).toBe(48754); + expect(journeyItemVO.lon).toBe(6.17651); + expect(journeyItemVO.lat).toBe(48.689445); + expect(journeyItemVO.actorTimes[0].firstMaxDatetime.getMinutes()).toBe(15); + }); + it('should throw an error if actorTimes is too short', () => { + expect( + () => + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 1545, + distance: 48754, + actorTimes: [], + }), + ).toThrow(ArgumentOutOfRangeException); + }); +}); diff --git a/src/modules/ad/tests/unit/core/journey.completer.spec.ts b/src/modules/ad/tests/unit/core/journey.completer.spec.ts index 5adbeaa..de6dfd9 100644 --- a/src/modules/ad/tests/unit/core/journey.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.completer.spec.ts @@ -90,14 +90,14 @@ const candidate: CandidateEntity = CandidateEntity.create({ driverDuration: 13548, driverSchedule: [ { - day: 0, + day: 1, time: '07:00', margin: 900, }, ], passengerSchedule: [ { - day: 0, + day: 1, time: '07:10', margin: 900, }, @@ -140,6 +140,7 @@ const candidate: CandidateEntity = CandidateEntity.create({ ], }, ]); +candidate.createJourneys = jest.fn().mockImplementation(() => candidate); describe('Journey completer', () => { it('should complete candidates with their journey', async () => { diff --git a/src/modules/ad/tests/unit/core/journey.value-object.spec.ts b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts index 5f8ec1e..3d321e5 100644 --- a/src/modules/ad/tests/unit/core/journey.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts @@ -5,161 +5,453 @@ import { import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; describe('Journey value object', () => { it('should create a journey value object', () => { const journeyVO = new Journey({ - firstDate: new Date('2023-09-20'), - lastDate: new Date('2024-09-20'), - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - time: '07:00', - minTime: '06:45', - maxTime: '07:15', + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + day: 5, + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.START, - time: '07:10', - minTime: '06:55', - maxTime: '07:25', + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], }), - new ActorTime({ - role: Role.DRIVER, - target: Target.FINISH, - time: '08:30', - minTime: '08:15', - maxTime: '08:45', + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.FINISH, - time: '08:40', - minTime: '08:25', - maxTime: '08:55', + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], }), ], - day: 0, - time: '07:00', - margin: 900, }); - expect(journeyVO.day).toBe(0); - expect(journeyVO.time).toBe('07:00'); - expect(journeyVO.margin).toBe(900); - expect(journeyVO.actorTimes).toHaveLength(4); - expect(journeyVO.firstDate.getDate()).toBe(20); - expect(journeyVO.lastDate.getMonth()).toBe(8); + expect(journeyVO.day).toBe(5); + expect(journeyVO.journeyItems).toHaveLength(4); + expect(journeyVO.firstDate.getDate()).toBe(1); + expect(journeyVO.lastDate.getMonth()).toBe(7); }); - it('should throw an exception if day is invalid', () => { - expect(() => { - new Journey({ - firstDate: new Date('2023-09-20'), - lastDate: new Date('2024-09-20'), - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - time: '07:00', - minTime: '06:45', - maxTime: '07:15', - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.START, - time: '07:10', - minTime: '06:55', - maxTime: '07:25', - }), - new ActorTime({ - role: Role.DRIVER, - target: Target.FINISH, - time: '08:30', - minTime: '08:15', - maxTime: '08:45', - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.FINISH, - time: '08:40', - minTime: '08:25', - maxTime: '08:55', - }), - ], - day: 7, - time: '07:00', - margin: 900, - }); - }).toThrow(ArgumentOutOfRangeException); + it('should throw an error if day is wrong', () => { + expect( + () => + new Journey({ + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + day: 7, + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }), + ).toThrow(ArgumentOutOfRangeException); }); - it('should throw an exception if actor times is too short', () => { - expect(() => { - new Journey({ - firstDate: new Date('2023-09-20'), - lastDate: new Date('2024-09-20'), - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - time: '07:00', - minTime: '06:45', - maxTime: '07:15', - }), - new ActorTime({ - role: Role.DRIVER, - target: Target.FINISH, - time: '08:30', - minTime: '08:15', - maxTime: '08:45', - }), - ], - day: 0, - time: '07:00', - margin: 900, - }); - }).toThrow(ArgumentOutOfRangeException); + it('should throw an error if dates are inconsistent', () => { + expect( + () => + new Journey({ + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-31'), + day: 5, + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }), + ).toThrow(ArgumentInvalidException); + expect( + () => + new Journey({ + firstDate: new Date('2024-08-30'), + lastDate: new Date('2023-09-01'), + day: 5, + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }), + ).toThrow(ArgumentInvalidException); }); - it('should throw an exception if dates are invalid', () => { - expect(() => { - new Journey({ - firstDate: new Date('2023-09-20'), - lastDate: new Date('2023-09-19'), - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - time: '07:00', - minTime: '06:45', - maxTime: '07:15', - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.START, - time: '07:10', - minTime: '06:55', - maxTime: '07:25', - }), - new ActorTime({ - role: Role.DRIVER, - target: Target.FINISH, - time: '08:30', - minTime: '08:15', - maxTime: '08:45', - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.FINISH, - time: '08:40', - minTime: '08:25', - maxTime: '08:55', - }), - ], - day: 0, - time: '07:00', - margin: 900, - }); - }).toThrow(ArgumentInvalidException); + it('should throw an error if journeyItems is too short', () => { + expect( + () => + new Journey({ + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + day: 5, + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + ], + }), + ).toThrow(ArgumentInvalidException); }); }); diff --git a/src/modules/ad/tests/unit/core/point.value-object.spec.ts b/src/modules/ad/tests/unit/core/point.value-object.spec.ts index 7f1fda0..8ae5913 100644 --- a/src/modules/ad/tests/unit/core/point.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/point.value-object.spec.ts @@ -23,8 +23,8 @@ describe('Point value object', () => { lat: 48.689446, lon: 6.17651, }); - expect(pointVO.isSame(identicalPointVO)).toBeTruthy(); - expect(pointVO.isSame(differentPointVO)).toBeFalsy(); + expect(pointVO.equals(identicalPointVO)).toBeTruthy(); + expect(pointVO.equals(differentPointVO)).toBeFalsy(); }); it('should throw an exception if longitude is invalid', () => { expect(() => { From 467d8a84f8aee2fc177ab71dcffd5db4f8a4d0c5 Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 22 Sep 2023 16:37:52 +0200 Subject: [PATCH 42/52] wip - create journeys - no tests yet --- .../queries/match/algorithm.abstract.ts | 2 +- .../match/completer/journey.completer.ts | 4 +- .../match/completer/route.completer.ts | 10 +- .../application/queries/match/match.query.ts | 2 +- .../selector/passenger-oriented.selector.ts | 46 ++-- .../ad/core/domain/calendar-tools.service.ts | 105 +++++--- .../ad/core/domain/candidate.entity.ts | 201 ++++++++++++--- src/modules/ad/core/domain/candidate.types.ts | 23 +- .../domain/carpool-path-creator.service.ts | 234 ++++++++++-------- .../value-objects/actor-time.value-object.ts | 2 +- ...t.ts => carpool-path-item.value-object.ts} | 22 +- .../value-objects/journey.value-object.ts | 15 +- .../input-datetime-transformer.ts | 2 +- .../ad/infrastructure/time-converter.ts | 4 +- .../unit/core/actor-time.value-object.spec.ts | 96 +++---- .../unit/core/algorithm.abstract.spec.ts | 4 + .../unit/core/calendar-tools.service.spec.ts | 201 +++++++++++---- .../tests/unit/core/candidate.entity.spec.ts | 220 ++++++++-------- .../core/carpool-path-creator.service.spec.ts | 72 +++--- ...=> carpool-path-item.value-object.spec.ts} | 37 ++- .../core/journey-item.value-object.spec.ts | 4 +- .../tests/unit/core/journey.completer.spec.ts | 17 +- .../unit/core/journey.value-object.spec.ts | 118 +-------- ...er-oriented-carpool-path-completer.spec.ts | 8 + .../passenger-oriented-geo-filter.spec.ts | 4 + .../tests/unit/core/route.completer.spec.ts | 17 +- .../infrastructure/graphhopper-georouter.ts | 12 +- 27 files changed, 860 insertions(+), 622 deletions(-) rename src/modules/ad/core/domain/value-objects/{carpool-step.value-object.ts => carpool-path-item.value-object.ts} (61%) rename src/modules/ad/tests/unit/core/{carpool-step.value-object.spec.ts => carpool-path-item.value-object.spec.ts} (60%) diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index faeb9bc..6915366 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -20,7 +20,7 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } - // console.log(JSON.stringify(this.candidates, null, 2)); + console.log(JSON.stringify(this.candidates, null, 2)); return this.candidates.map((candidate: CandidateEntity) => MatchEntity.create({ adId: candidate.id }), ); diff --git a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts index b50ebf5..71469bc 100644 --- a/src/modules/ad/core/application/queries/match/completer/journey.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/journey.completer.ts @@ -5,7 +5,5 @@ export class JourneyCompleter extends Completer { complete = async ( candidates: CandidateEntity[], ): Promise => - candidates.map((candidate: CandidateEntity) => - candidate.createJourneys(this.query.fromDate, this.query.toDate), - ); + candidates.map((candidate: CandidateEntity) => candidate.createJourneys()); } diff --git a/src/modules/ad/core/application/queries/match/completer/route.completer.ts b/src/modules/ad/core/application/queries/match/completer/route.completer.ts index 96dc9f1..5a07e10 100644 --- a/src/modules/ad/core/application/queries/match/completer/route.completer.ts +++ b/src/modules/ad/core/application/queries/match/completer/route.completer.ts @@ -1,8 +1,8 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Completer } from './completer.abstract'; import { MatchQuery } from '../match.query'; -import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object'; import { Step } from '../../../types/step.type'; +import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object'; export class RouteCompleter extends Completer { protected readonly type: RouteCompleterType; @@ -19,8 +19,8 @@ export class RouteCompleter extends Completer { switch (this.type) { case RouteCompleterType.BASIC: const basicCandidateRoute = await this.query.routeProvider.getBasic( - (candidate.getProps().carpoolSteps as CarpoolStep[]).map( - (carpoolStep: CarpoolStep) => carpoolStep.point, + (candidate.getProps().carpoolPath as CarpoolPathItem[]).map( + (carpoolPathItem: CarpoolPathItem) => carpoolPathItem, ), ); candidate.setMetrics( @@ -31,8 +31,8 @@ export class RouteCompleter extends Completer { case RouteCompleterType.DETAILED: const detailedCandidateRoute = await this.query.routeProvider.getDetailed( - (candidate.getProps().carpoolSteps as CarpoolStep[]).map( - (carpoolStep: CarpoolStep) => carpoolStep.point, + (candidate.getProps().carpoolPath as CarpoolPathItem[]).map( + (carpoolPathItem: CarpoolPathItem) => carpoolPathItem, ), ); candidate.setSteps(detailedCandidateRoute.steps as Step[]); diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 337ae7c..742a663 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -153,7 +153,7 @@ export class MatchQuery extends QueryBase { ); this.schedule = this.schedule.map((scheduleItem: ScheduleItem) => ({ day: datetimeTransformer.day( - scheduleItem.day ?? new Date(this.fromDate).getDay(), + scheduleItem.day ?? new Date(this.fromDate).getUTCDay(), { date: this.fromDate, time: scheduleItem.time, diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 7768fba..ebb1cbd 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -36,6 +36,16 @@ export class PassengerOrientedSelector extends Selector { CandidateEntity.create({ id: adEntity.id, role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER, + dateInterval: { + lowerDate: this._maxDateString( + this.query.fromDate, + adEntity.getProps().fromDate, + ), + higherDate: this._minDateString( + this.query.toDate, + adEntity.getProps().toDate, + ), + }, driverWaypoints: adsRole.role == Role.PASSENGER ? adEntity.getProps().waypoints @@ -173,7 +183,7 @@ export class PassengerOrientedSelector extends Selector { scheduleDates.map((date: Date) => { this.query.schedule .filter( - (scheduleItem: ScheduleItem) => date.getDay() == scheduleItem.day, + (scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day, ) .map((scheduleItem: ScheduleItem) => { switch (role) { @@ -205,15 +215,15 @@ export class PassengerOrientedSelector extends Selector { ); // we want the min departure time of the driver to be before the max departure time of the passenger return `make_timestamp(\ - ${maxDepartureDatetime.getFullYear()},\ - ${maxDepartureDatetime.getMonth() + 1},\ - ${maxDepartureDatetime.getDate()},\ + ${maxDepartureDatetime.getUTCFullYear()},\ + ${maxDepartureDatetime.getUTCMonth() + 1},\ + ${maxDepartureDatetime.getUTCDate()},\ CAST(EXTRACT(hour from time) as integer),\ CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\ make_timestamp(\ - ${maxDepartureDatetime.getFullYear()},\ - ${maxDepartureDatetime.getMonth() + 1},\ - ${maxDepartureDatetime.getDate()},${maxDepartureDatetime.getHours()},${maxDepartureDatetime.getMinutes()},0)`; + ${maxDepartureDatetime.getUTCFullYear()},\ + ${maxDepartureDatetime.getUTCMonth() + 1},\ + ${maxDepartureDatetime.getUTCDate()},${maxDepartureDatetime.getUTCHours()},${maxDepartureDatetime.getUTCMinutes()},0)`; }; private _whereDriverSchedule = ( @@ -229,15 +239,15 @@ export class PassengerOrientedSelector extends Selector { ); // we want the max departure time of the passenger to be after the min departure time of the driver return `make_timestamp(\ - ${minDepartureDatetime.getFullYear()}, - ${minDepartureDatetime.getMonth() + 1}, - ${minDepartureDatetime.getDate()},\ + ${minDepartureDatetime.getUTCFullYear()}, + ${minDepartureDatetime.getUTCMonth() + 1}, + ${minDepartureDatetime.getUTCDate()},\ CAST(EXTRACT(hour from time) as integer),\ CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\ make_timestamp(\ - ${minDepartureDatetime.getFullYear()}, - ${minDepartureDatetime.getMonth() + 1}, - ${minDepartureDatetime.getDate()},${minDepartureDatetime.getHours()},${minDepartureDatetime.getMinutes()},0)`; + ${minDepartureDatetime.getUTCFullYear()}, + ${minDepartureDatetime.getUTCMonth() + 1}, + ${minDepartureDatetime.getUTCDate()},${minDepartureDatetime.getUTCHours()},${minDepartureDatetime.getUTCMinutes()},0)`; }; private _whereAzimuth = (): string => { @@ -311,7 +321,7 @@ export class PassengerOrientedSelector extends Selector { for ( let date = fromDate; date <= toDate; - date.setDate(date.getDate() + 1) + date.setUTCDate(date.getUTCDate() + 1) ) { dates.push(new Date(date)); count++; @@ -321,7 +331,7 @@ export class PassengerOrientedSelector extends Selector { }; private _addMargin = (date: Date, marginInSeconds: number): Date => { - date.setTime(date.getTime() + marginInSeconds * 1000); + date.setUTCSeconds(marginInSeconds); return date; }; @@ -334,6 +344,12 @@ export class PassengerOrientedSelector extends Selector { maxAzimuth: azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin, }); + + private _maxDateString = (date1: string, date2: string): string => + new Date(date1) > new Date(date2) ? date1 : date2; + + private _minDateString = (date1: string, date2: string): string => + new Date(date1) < new Date(date2) ? date1 : date2; } export type QueryStringRole = { diff --git a/src/modules/ad/core/domain/calendar-tools.service.ts b/src/modules/ad/core/domain/calendar-tools.service.ts index b0d2b4f..184adc6 100644 --- a/src/modules/ad/core/domain/calendar-tools.service.ts +++ b/src/modules/ad/core/domain/calendar-tools.service.ts @@ -1,31 +1,29 @@ import { ExceptionBase } from '@mobicoop/ddd-library'; +import { DateInterval } from './candidate.types'; export class CalendarTools { /** * Returns the first date corresponding to a week day (0 based monday) * within a date range */ - static firstDate = ( - weekDay: number, - lowerDate: string, - higherDate: string, - ): Date => { + static firstDate = (weekDay: number, dateInterval: DateInterval): Date => { if (weekDay < 0 || weekDay > 6) throw new CalendarToolsException( new Error('weekDay must be between 0 and 6'), ); - const lowerDateAsDate: Date = new Date(lowerDate); - const higherDateAsDate: Date = new Date(higherDate); - if (lowerDateAsDate.getDay() == weekDay) return lowerDateAsDate; + const lowerDateAsDate: Date = new Date(dateInterval.lowerDate); + const higherDateAsDate: Date = new Date(dateInterval.higherDate); + if (lowerDateAsDate.getUTCDay() == weekDay) return lowerDateAsDate; const nextDate: Date = new Date(lowerDateAsDate); - nextDate.setDate( - lowerDateAsDate.getDate() + (7 - (lowerDateAsDate.getDay() - weekDay)), + nextDate.setUTCDate( + lowerDateAsDate.getUTCDate() + + (7 - (lowerDateAsDate.getUTCDay() - weekDay)), ); - if (lowerDateAsDate.getDay() < weekDay) { - nextDate.setMonth(lowerDateAsDate.getMonth()); - nextDate.setFullYear(lowerDateAsDate.getFullYear()); - nextDate.setDate( - lowerDateAsDate.getDate() + (weekDay - lowerDateAsDate.getDay()), + if (lowerDateAsDate.getUTCDay() < weekDay) { + nextDate.setUTCMonth(lowerDateAsDate.getUTCMonth()); + nextDate.setUTCFullYear(lowerDateAsDate.getUTCFullYear()); + nextDate.setUTCDate( + lowerDateAsDate.getUTCDate() + (weekDay - lowerDateAsDate.getUTCDay()), ); } if (nextDate <= higherDateAsDate) return nextDate; @@ -38,28 +36,24 @@ export class CalendarTools { * Returns the last date corresponding to a week day (0 based monday) * within a date range */ - static lastDate = ( - weekDay: number, - lowerDate: string, - higherDate: string, - ): Date => { + static lastDate = (weekDay: number, dateInterval: DateInterval): Date => { if (weekDay < 0 || weekDay > 6) throw new CalendarToolsException( new Error('weekDay must be between 0 and 6'), ); - const lowerDateAsDate: Date = new Date(lowerDate); - const higherDateAsDate: Date = new Date(higherDate); - if (higherDateAsDate.getDay() == weekDay) return higherDateAsDate; + const lowerDateAsDate: Date = new Date(dateInterval.lowerDate); + const higherDateAsDate: Date = new Date(dateInterval.higherDate); + if (higherDateAsDate.getUTCDay() == weekDay) return higherDateAsDate; const previousDate: Date = new Date(higherDateAsDate); - previousDate.setDate( - higherDateAsDate.getDate() - (higherDateAsDate.getDay() - weekDay), + previousDate.setUTCDate( + higherDateAsDate.getUTCDate() - (higherDateAsDate.getUTCDay() - weekDay), ); - if (higherDateAsDate.getDay() < weekDay) { - previousDate.setMonth(higherDateAsDate.getMonth()); - previousDate.setFullYear(higherDateAsDate.getFullYear()); - previousDate.setDate( - higherDateAsDate.getDate() - - (7 + (higherDateAsDate.getDay() - weekDay)), + if (higherDateAsDate.getUTCDay() < weekDay) { + previousDate.setUTCMonth(higherDateAsDate.getUTCMonth()); + previousDate.setUTCFullYear(higherDateAsDate.getUTCFullYear()); + previousDate.setUTCDate( + higherDateAsDate.getUTCDate() - + (7 + (higherDateAsDate.getUTCDay() - weekDay)), ); } if (previousDate >= lowerDateAsDate) return previousDate; @@ -67,6 +61,55 @@ export class CalendarTools { new Error('no available day for the given date range'), ); }; + + /** + * Returns a date from a date and time as strings, adding optional seconds + */ + static datetimeFromString = ( + date: string, + time: string, + additionalSeconds = 0, + ): Date => { + const datetime = new Date(`${date}T${time}:00Z`); + datetime.setUTCSeconds(additionalSeconds); + return datetime; + }; + + /** + * Returns dates from a day and time based on unix epoch day + * (1970-01-01 is day 4) + * The method returns an array of dates because for edges (day 0 and 6) + * we need to return 2 possibilities + */ + static epochDaysFromTime = (weekDay: number, time: string): Date[] => { + if (weekDay < 0 || weekDay > 6) + throw new CalendarToolsException( + new Error('weekDay must be between 0 and 6'), + ); + switch (weekDay) { + case 0: + return [ + new Date(`1969-12-28T${time}:00Z`), + new Date(`1970-01-04T${time}:00Z`), + ]; + case 1: + return [new Date(`1969-12-29T${time}:00Z`)]; + case 2: + return [new Date(`1969-12-30T${time}:00Z`)]; + case 3: + return [new Date(`1969-12-31T${time}:00Z`)]; + case 5: + return [new Date(`1970-01-02T${time}:00Z`)]; + case 6: + return [ + new Date(`1969-12-27T${time}:00Z`), + new Date(`1970-01-03T${time}:00Z`), + ]; + case 4: + default: + return [new Date(`1970-01-01T${time}:00Z`)]; + } + }; } export class CalendarToolsException extends ExceptionBase { diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 4bd0143..073dcea 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -1,11 +1,21 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; -import { CandidateProps, CreateCandidateProps } from './candidate.types'; -import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; -import { StepProps } from './value-objects/step.value-object'; +import { + CandidateProps, + CreateCandidateProps, + Target, +} from './candidate.types'; +import { + CarpoolPathItem, + CarpoolPathItemProps, +} from './value-objects/carpool-path-item.value-object'; +import { Step, StepProps } from './value-objects/step.value-object'; import { ScheduleItem } from './value-objects/schedule-item.value-object'; import { Journey } from './value-objects/journey.value-object'; import { CalendarTools } from './calendar-tools.service'; import { JourneyItem } from './value-objects/journey-item.value-object'; +import { Actor } from './value-objects/actor.value-object'; +import { ActorTime } from './value-objects/actor-time.value-object'; +import { Role } from './ad.types'; export class CandidateEntity extends AggregateRoot { protected readonly _id: AggregateID; @@ -15,8 +25,8 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; - setCarpoolPath = (carpoolSteps: CarpoolStepProps[]): CandidateEntity => { - this.props.carpoolSteps = carpoolSteps; + setCarpoolPath = (carpoolPath: CarpoolPathItemProps[]): CandidateEntity => { + this.props.carpoolPath = carpoolPath; return this; }; @@ -34,21 +44,14 @@ export class CandidateEntity extends AggregateRoot { isDetourValid = (): boolean => this._validateDistanceDetour() && this._validateDurationDetour(); - createJourneys = (fromDate: string, toDate: string): CandidateEntity => { - this.props.driverJourneys = this.props.driverSchedule - .map((driverScheduleItem: ScheduleItem) => - this._createJourney(fromDate, toDate, driverScheduleItem), - ) - .filter( - (journey: Journey | undefined) => journey !== undefined, - ) as Journey[]; - this.props.passengerJourneys = this.props.passengerSchedule - .map((passengerScheduleItem: ScheduleItem) => - this._createJourney(fromDate, toDate, passengerScheduleItem), - ) - .filter( - (journey: Journey | undefined) => journey !== undefined, - ) as Journey[]; + /** + * Create the journeys based on the driver schedule (the driver 'drives' the carpool !) + */ + createJourneys = (): CandidateEntity => { + this.props.journeys = this.props.driverSchedule.map( + (driverScheduleItem: ScheduleItem) => + this._createJourney(driverScheduleItem), + ); return this; }; @@ -66,23 +69,159 @@ export class CandidateEntity extends AggregateRoot { (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) : false; - private _createJourney = ( - fromDate: string, - toDate: string, - scheduleItem: ScheduleItem, - ): Journey | undefined => + private _createJourney = (driverScheduleItem: ScheduleItem): Journey => new Journey({ - day: scheduleItem.day, - firstDate: CalendarTools.firstDate(scheduleItem.day, fromDate, toDate), - lastDate: CalendarTools.lastDate(scheduleItem.day, fromDate, toDate), - journeyItems: this._createJourneyItems(scheduleItem), + firstDate: CalendarTools.firstDate( + driverScheduleItem.day, + this.props.dateInterval, + ), + lastDate: CalendarTools.lastDate( + driverScheduleItem.day, + this.props.dateInterval, + ), + journeyItems: this._createJourneyItems(driverScheduleItem), }); private _createJourneyItems = ( - scheduleItem: ScheduleItem, - ): JourneyItem[] => []; + driverScheduleItem: ScheduleItem, + ): JourneyItem[] => + this.props.carpoolPath?.map( + (carpoolPathItem: CarpoolPathItem, index: number) => + this._createJourneyItem(carpoolPathItem, index, driverScheduleItem), + ) as JourneyItem[]; + + private _createJourneyItem = ( + carpoolPathItem: CarpoolPathItem, + stepIndex: number, + driverScheduleItem: ScheduleItem, + ): JourneyItem => + new JourneyItem({ + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, + duration: ((this.props.steps as Step[])[stepIndex] as Step).duration, + distance: ((this.props.steps as Step[])[stepIndex] as Step).distance, + actorTimes: carpoolPathItem.actors.map((actor: Actor) => + this._createActorTime( + actor, + driverScheduleItem, + ((this.props.steps as Step[])[stepIndex] as Step).duration, + ), + ), + }); + + private _createActorTime = ( + actor: Actor, + driverScheduleItem: ScheduleItem, + duration: number, + ): ActorTime => { + const scheduleItem: ScheduleItem = + actor.role == Role.PASSENGER && actor.target == Target.START + ? this._closestPassengerScheduleItem(driverScheduleItem) + : driverScheduleItem; + const effectiveDuration = + (actor.role == Role.PASSENGER && actor.target == Target.START) || + actor.target == Target.START + ? 0 + : duration; + return new ActorTime({ + role: actor.role, + target: actor.target, + firstDatetime: CalendarTools.datetimeFromString( + this.props.dateInterval.lowerDate, + scheduleItem.time, + effectiveDuration, + ), + firstMinDatetime: CalendarTools.datetimeFromString( + this.props.dateInterval.lowerDate, + scheduleItem.time, + -scheduleItem.margin + effectiveDuration, + ), + firstMaxDatetime: CalendarTools.datetimeFromString( + this.props.dateInterval.lowerDate, + scheduleItem.time, + scheduleItem.margin + effectiveDuration, + ), + lastDatetime: CalendarTools.datetimeFromString( + this.props.dateInterval.higherDate, + scheduleItem.time, + effectiveDuration, + ), + lastMinDatetime: CalendarTools.datetimeFromString( + this.props.dateInterval.higherDate, + scheduleItem.time, + -scheduleItem.margin + effectiveDuration, + ), + lastMaxDatetime: CalendarTools.datetimeFromString( + this.props.dateInterval.higherDate, + scheduleItem.time, + scheduleItem.margin + effectiveDuration, + ), + }); + }; + + private _closestPassengerScheduleItem = ( + driverScheduleItem: ScheduleItem, + ): ScheduleItem => + CalendarTools.epochDaysFromTime( + driverScheduleItem.day, + driverScheduleItem.time, + ) + .map((driverDate: Date) => + this._minPassengerScheduleItemGapForDate(driverDate), + ) + .reduce( + ( + previousScheduleItemGap: ScheduleItemGap, + currentScheduleItemGap: ScheduleItemGap, + ) => + previousScheduleItemGap.gap < currentScheduleItemGap.gap + ? previousScheduleItemGap + : currentScheduleItemGap, + ).scheduleItem; + + private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap => + this.props.passengerSchedule + .map( + (scheduleItem: ScheduleItem) => + { + scheduleItem, + range: CalendarTools.epochDaysFromTime( + scheduleItem.day, + scheduleItem.time, + ), + }, + ) + .map((scheduleItemRange: ScheduleItemRange) => ({ + scheduleItem: scheduleItemRange.scheduleItem, + gap: scheduleItemRange.range + .map((scheduleDate: Date) => + Math.round(Math.abs(scheduleDate.getTime() - date.getTime())), + ) + .reduce((previousGap: number, currentGap: number) => + previousGap < currentGap ? previousGap : currentGap, + ), + })) + .reduce( + ( + previousScheduleItemGap: ScheduleItemGap, + currentScheduleItemGap: ScheduleItemGap, + ) => + previousScheduleItemGap.gap < currentScheduleItemGap.gap + ? previousScheduleItemGap + : currentScheduleItemGap, + ); validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } } + +type ScheduleItemRange = { + scheduleItem: ScheduleItem; + range: Date[]; +}; + +type ScheduleItemGap = { + scheduleItem: ScheduleItem; + gap: number; +}; diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 07b5d27..c9bc237 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,7 +1,7 @@ import { Role } from './ad.types'; import { PointProps } from './value-objects/point.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; -import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; +import { CarpoolPathItemProps } from './value-objects/carpool-path-item.value-object'; import { JourneyProps } from './value-objects/journey.value-object'; import { StepProps } from './value-objects/step.value-object'; @@ -10,16 +10,16 @@ export interface CandidateProps { role: Role; driverWaypoints: PointProps[]; passengerWaypoints: PointProps[]; + driverSchedule: ScheduleItemProps[]; + passengerSchedule: ScheduleItemProps[]; driverDistance: number; driverDuration: number; - carpoolSteps?: CarpoolStepProps[]; + dateInterval: DateInterval; + carpoolPath?: CarpoolPathItemProps[]; distance?: number; duration?: number; steps?: StepProps[]; - driverSchedule: ScheduleItemProps[]; - passengerSchedule: ScheduleItemProps[]; - driverJourneys?: JourneyProps[]; - passengerJourneys?: JourneyProps[]; + journeys?: JourneyProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; } @@ -34,6 +34,7 @@ export interface CreateCandidateProps { driverSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[]; spacetimeDetourRatio: SpacetimeDetourRatio; + dateInterval: DateInterval; } export enum Target { @@ -47,12 +48,12 @@ export abstract class Validator { abstract validate(): boolean; } -export type SpacetimeMetric = { - distance: number; - duration: number; -}; - export type SpacetimeDetourRatio = { maxDistanceDetourRatio: number; maxDurationDetourRatio: number; }; + +export type DateInterval = { + lowerDate: string; + higherDate: string; +}; diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index 68a0a51..5448ae2 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -3,7 +3,7 @@ import { Target } from './candidate.types'; import { CarpoolPathCreatorException } from './match.errors'; import { Actor } from './value-objects/actor.value-object'; import { Point } from './value-objects/point.value-object'; -import { CarpoolStep } from './value-objects/carpool-step.value-object'; +import { CarpoolPathItem } from './value-objects/carpool-path-item.value-object'; export class CarpoolPathCreator { private PRECISION = 5; @@ -23,39 +23,34 @@ export class CarpoolPathCreator { } /** - * Creates a path (a list of carpoolSteps) between driver waypoints + * Creates a path (a list of carpoolPathItem) between driver waypoints and passenger waypoints respecting the order of the driver waypoints Inspired by : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment */ - public carpoolPath = (): CarpoolStep[] => + public carpoolPath = (): CarpoolPathItem[] => this._consolidate( - this._mixedCarpoolSteps( - this._driverCarpoolSteps(), - this._passengerCarpoolSteps(), + this._mixedCarpoolPath( + this._driverCarpoolPath(), + this._passengerCarpoolPath(), ), ); - private _mixedCarpoolSteps = ( - driverCarpoolSteps: CarpoolStep[], - passengerCarpoolSteps: CarpoolStep[], - ): CarpoolStep[] => - driverCarpoolSteps.length == 2 - ? this._simpleMixedCarpoolSteps(driverCarpoolSteps, passengerCarpoolSteps) - : this._complexMixedCarpoolSteps( - driverCarpoolSteps, - passengerCarpoolSteps, - ); + private _mixedCarpoolPath = ( + driverCarpoolPath: CarpoolPathItem[], + passengerCarpoolPath: CarpoolPathItem[], + ): CarpoolPathItem[] => + driverCarpoolPath.length == 2 + ? this._simpleMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath) + : this._complexMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath); - private _driverCarpoolSteps = (): CarpoolStep[] => + private _driverCarpoolPath = (): CarpoolPathItem[] => this.driverWaypoints.map( (waypoint: Point, index: number) => - new CarpoolStep({ - point: new Point({ - lon: waypoint.lon, - lat: waypoint.lat, - }), + new CarpoolPathItem({ + lon: waypoint.lon, + lat: waypoint.lat, actors: [ new Actor({ role: Role.DRIVER, @@ -66,17 +61,15 @@ export class CarpoolPathCreator { ); /** - * Creates the passenger carpoolSteps with original passenger waypoints, adding driver waypoints that are the same + * Creates the passenger carpoolPath with original passenger waypoints, adding driver waypoints that are the same */ - private _passengerCarpoolSteps = (): CarpoolStep[] => { - const carpoolSteps: CarpoolStep[] = []; + private _passengerCarpoolPath = (): CarpoolPathItem[] => { + const carpoolPath: CarpoolPathItem[] = []; this.passengerWaypoints.forEach( (passengerWaypoint: Point, index: number) => { - const carpoolStep: CarpoolStep = new CarpoolStep({ - point: new Point({ - lon: passengerWaypoint.lon, - lat: passengerWaypoint.lat, - }), + const carpoolPathItem: CarpoolPathItem = new CarpoolPathItem({ + lon: passengerWaypoint.lon, + lat: passengerWaypoint.lat, actors: [ new Actor({ role: Role.PASSENGER, @@ -89,78 +82,80 @@ export class CarpoolPathCreator { passengerWaypoint.equals(driverWaypoint), ).length == 0 ) { - carpoolStep.actors.push( + carpoolPathItem.actors.push( new Actor({ role: Role.DRIVER, target: Target.NEUTRAL, }), ); } - carpoolSteps.push(carpoolStep); + carpoolPath.push(carpoolPathItem); }, ); - return carpoolSteps; + return carpoolPath; }; - private _simpleMixedCarpoolSteps = ( - driverCarpoolSteps: CarpoolStep[], - passengerCarpoolSteps: CarpoolStep[], - ): CarpoolStep[] => [ - driverCarpoolSteps[0], - ...passengerCarpoolSteps, - driverCarpoolSteps[1], + private _simpleMixedCarpoolPath = ( + driverCarpoolPath: CarpoolPathItem[], + passengerCarpoolPath: CarpoolPathItem[], + ): CarpoolPathItem[] => [ + driverCarpoolPath[0], + ...passengerCarpoolPath, + driverCarpoolPath[1], ]; - private _complexMixedCarpoolSteps = ( - driverCarpoolSteps: CarpoolStep[], - passengerCarpoolSteps: CarpoolStep[], - ): CarpoolStep[] => { - let mixedCarpoolSteps: CarpoolStep[] = [...driverCarpoolSteps]; + private _complexMixedCarpoolPath = ( + driverCarpoolPath: CarpoolPathItem[], + passengerCarpoolPath: CarpoolPathItem[], + ): CarpoolPathItem[] => { + let mixedCarpoolPath: CarpoolPathItem[] = [...driverCarpoolPath]; const originInsertIndex: number = this._insertIndex( - passengerCarpoolSteps[0], - driverCarpoolSteps, + passengerCarpoolPath[0], + driverCarpoolPath, ); - mixedCarpoolSteps = [ - ...mixedCarpoolSteps.slice(0, originInsertIndex), - passengerCarpoolSteps[0], - ...mixedCarpoolSteps.slice(originInsertIndex), + mixedCarpoolPath = [ + ...mixedCarpoolPath.slice(0, originInsertIndex), + passengerCarpoolPath[0], + ...mixedCarpoolPath.slice(originInsertIndex), ]; const destinationInsertIndex: number = this._insertIndex( - passengerCarpoolSteps[passengerCarpoolSteps.length - 1], - driverCarpoolSteps, + passengerCarpoolPath[passengerCarpoolPath.length - 1], + driverCarpoolPath, ) + 1; - mixedCarpoolSteps = [ - ...mixedCarpoolSteps.slice(0, destinationInsertIndex), - passengerCarpoolSteps[passengerCarpoolSteps.length - 1], - ...mixedCarpoolSteps.slice(destinationInsertIndex), + mixedCarpoolPath = [ + ...mixedCarpoolPath.slice(0, destinationInsertIndex), + passengerCarpoolPath[passengerCarpoolPath.length - 1], + ...mixedCarpoolPath.slice(destinationInsertIndex), ]; - return mixedCarpoolSteps; + return mixedCarpoolPath; }; private _insertIndex = ( - targetCarpoolStep: CarpoolStep, - carpoolSteps: CarpoolStep[], + targetCarpoolPathItem: CarpoolPathItem, + carpoolPath: CarpoolPathItem[], ): number => - this._closestSegmentIndex(targetCarpoolStep, this._segments(carpoolSteps)) + - 1; + this._closestSegmentIndex( + targetCarpoolPathItem, + this._segments(carpoolPath), + ) + 1; - private _segments = (carpoolSteps: CarpoolStep[]): CarpoolStep[][] => { - const segments: CarpoolStep[][] = []; - carpoolSteps.forEach((carpoolStep: CarpoolStep, index: number) => { - if (index < carpoolSteps.length - 1) - segments.push([carpoolStep, carpoolSteps[index + 1]]); + private _segments = (carpoolPath: CarpoolPathItem[]): CarpoolPathItem[][] => { + const segments: CarpoolPathItem[][] = []; + carpoolPath.forEach((carpoolPathItem: CarpoolPathItem, index: number) => { + if (index < carpoolPath.length - 1) + segments.push([carpoolPathItem, carpoolPath[index + 1]]); }); return segments; }; private _closestSegmentIndex = ( - carpoolStep: CarpoolStep, - segments: CarpoolStep[][], + carpoolPathItem: CarpoolPathItem, + segments: CarpoolPathItem[][], ): number => { const distances: Map = new Map(); - segments.forEach((segment: CarpoolStep[], index: number) => { - distances.set(index, this._distanceToSegment(carpoolStep, segment)); + segments.forEach((segment: CarpoolPathItem[], index: number) => { + distances.set(index, this._distanceToSegment(carpoolPathItem, segment)); }); const sortedDistances: Map = new Map( [...distances.entries()].sort((a, b) => a[1] - b[1]), @@ -170,45 +165,62 @@ export class CarpoolPathCreator { }; private _distanceToSegment = ( - carpoolStep: CarpoolStep, - segment: CarpoolStep[], + carpoolPathItem: CarpoolPathItem, + segment: CarpoolPathItem[], ): number => parseFloat( - Math.sqrt(this._distanceToSegmentSquared(carpoolStep, segment)).toFixed( - this.PRECISION, - ), + Math.sqrt( + this._distanceToSegmentSquared(carpoolPathItem, segment), + ).toFixed(this.PRECISION), ); private _distanceToSegmentSquared = ( - carpoolStep: CarpoolStep, - segment: CarpoolStep[], + carpoolPathItem: CarpoolPathItem, + segment: CarpoolPathItem[], ): number => { const length2: number = this._distanceSquared( - segment[0].point, - segment[1].point, + new Point({ + lon: segment[0].lon, + lat: segment[0].lat, + }), + new Point({ + lon: segment[1].lon, + lat: segment[1].lat, + }), ); if (length2 == 0) - return this._distanceSquared(carpoolStep.point, segment[0].point); + return this._distanceSquared( + new Point({ + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, + }), + new Point({ + lon: segment[0].lon, + lat: segment[0].lat, + }), + ); const length: number = Math.max( 0, Math.min( 1, - ((carpoolStep.point.lon - segment[0].point.lon) * - (segment[1].point.lon - segment[0].point.lon) + - (carpoolStep.point.lat - segment[0].point.lat) * - (segment[1].point.lat - segment[0].point.lat)) / + ((carpoolPathItem.lon - segment[0].lon) * + (segment[1].lon - segment[0].lon) + + (carpoolPathItem.lat - segment[0].lat) * + (segment[1].lat - segment[0].lat)) / length2, ), ); const newPoint: Point = new Point({ - lon: - segment[0].point.lon + - length * (segment[1].point.lon - segment[0].point.lon), - lat: - segment[0].point.lat + - length * (segment[1].point.lat - segment[0].point.lat), + lon: segment[0].lon + length * (segment[1].lon - segment[0].lon), + lat: segment[0].lat + length * (segment[1].lat - segment[0].lat), }); - return this._distanceSquared(carpoolStep.point, newPoint); + return this._distanceSquared( + new Point({ + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, + }), + newPoint, + ); }; private _distanceSquared = (point1: Point, point2: Point): number => @@ -227,31 +239,43 @@ export class CarpoolPathCreator { : Target.INTERMEDIATE; /** - * Consolidate carpoolSteps by removing duplicate actors (eg. driver with neutral and start or finish target) + * Consolidate carpoolPath by removing duplicate actors (eg. driver with neutral and start or finish target) */ - private _consolidate = (carpoolSteps: CarpoolStep[]): CarpoolStep[] => { + private _consolidate = ( + carpoolPath: CarpoolPathItem[], + ): CarpoolPathItem[] => { const uniquePoints: Point[] = []; - carpoolSteps.forEach((carpoolStep: CarpoolStep) => { + carpoolPath.forEach((carpoolPathItem: CarpoolPathItem) => { if ( - uniquePoints.find((point: Point) => point.equals(carpoolStep.point)) === - undefined + uniquePoints.find((point: Point) => + point.equals( + new Point({ + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, + }), + ), + ) === undefined ) uniquePoints.push( new Point({ - lon: carpoolStep.point.lon, - lat: carpoolStep.point.lat, + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, }), ); }); return uniquePoints.map( (point: Point) => - new CarpoolStep({ - point, - actors: carpoolSteps - .filter((carpoolStep: CarpoolStep) => - carpoolStep.point.equals(point), + new CarpoolPathItem({ + lon: point.lon, + lat: point.lat, + actors: carpoolPath + .filter((carpoolPathItem: CarpoolPathItem) => + new Point({ + lon: carpoolPathItem.lon, + lat: carpoolPathItem.lat, + }).equals(point), ) - .map((carpoolStep: CarpoolStep) => carpoolStep.actors) + .map((carpoolPathItem: CarpoolPathItem) => carpoolPathItem.actors) .flat(), }), ); diff --git a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts index 9702dcb..41b0bfa 100644 --- a/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/actor-time.value-object.ts @@ -56,7 +56,7 @@ export class ActorTime extends ValueObject { role: props.role, target: props.target, }); - if (props.firstDatetime.getDay() != props.lastDatetime.getDay()) + if (props.firstDatetime.getUTCDay() != props.lastDatetime.getUTCDay()) throw new ArgumentInvalidException( 'firstDatetime week day must be equal to lastDatetime week day', ); diff --git a/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts similarity index 61% rename from src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts rename to src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts index 95382cf..bb7dafa 100644 --- a/src/modules/ad/core/domain/value-objects/carpool-step.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts @@ -4,28 +4,36 @@ import { } from '@mobicoop/ddd-library'; import { Actor } from './actor.value-object'; import { Role } from '../ad.types'; -import { Point } from './point.value-object'; +import { Point, PointProps } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain * other Value Objects inside if needed. * */ -export interface CarpoolStepProps { - point: Point; +export interface CarpoolPathItemProps extends PointProps { actors: Actor[]; } -export class CarpoolStep extends ValueObject { - get point(): Point { - return this.props.point; +export class CarpoolPathItem extends ValueObject { + get lon(): number { + return this.props.lon; + } + + get lat(): number { + return this.props.lat; } get actors(): Actor[] { return this.props.actors; } - protected validate(props: CarpoolStepProps): void { + protected validate(props: CarpoolPathItemProps): void { + // validate point props + new Point({ + lon: props.lon, + lat: props.lat, + }); if (props.actors.length <= 0) throw new ArgumentOutOfRangeException('at least one actor is required'); if ( diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts index fb7f421..df03132 100644 --- a/src/modules/ad/core/domain/value-objects/journey.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -1,8 +1,4 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, - ValueObject, -} from '@mobicoop/ddd-library'; +import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; import { JourneyItem } from './journey-item.value-object'; /** Note: @@ -13,7 +9,6 @@ import { JourneyItem } from './journey-item.value-object'; export interface JourneyProps { firstDate: Date; lastDate: Date; - day: number; journeyItems: JourneyItem[]; } @@ -26,18 +21,12 @@ export class Journey extends ValueObject { return this.props.lastDate; } - get day(): number { - return this.props.day; - } - get journeyItems(): JourneyItem[] { return this.props.journeyItems; } protected validate(props: JourneyProps): void { - if (props.day < 0 || props.day > 6) - throw new ArgumentOutOfRangeException('day must be between 0 and 6'); - if (props.firstDate.getDay() != props.lastDate.getDay()) + if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay()) throw new ArgumentInvalidException( 'firstDate week day must be equal to lastDate week day', ); diff --git a/src/modules/ad/infrastructure/input-datetime-transformer.ts b/src/modules/ad/infrastructure/input-datetime-transformer.ts index 97df366..5181a0a 100644 --- a/src/modules/ad/infrastructure/input-datetime-transformer.ts +++ b/src/modules/ad/infrastructure/input-datetime-transformer.ts @@ -79,7 +79,7 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort { this._defaultTimezone, )[0], ); - return new Date(this.fromDate(geoFromDate, frequency)).getDay(); + return new Date(this.fromDate(geoFromDate, frequency)).getUTCDay(); }; /** diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts index bb186de..fc3314f 100644 --- a/src/modules/ad/infrastructure/time-converter.ts +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -45,7 +45,7 @@ export class TimeConverter implements TimeConverterPort { .convert(TimeZone.zone('UTC')) .toIsoString() .split('T')[0], - ).getDay(); + ).getUTCDay(); localUnixEpochDayFromTime = (time: string, timezone: string): number => new Date( @@ -53,5 +53,5 @@ export class TimeConverter implements TimeConverterPort { .convert(TimeZone.zone(timezone)) .toIsoString() .split('T')[0], - ).getDay(); + ).getUTCDay(); } diff --git a/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts index 3d7ed4e..f41ccba 100644 --- a/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/actor-time.value-object.spec.ts @@ -8,21 +8,21 @@ describe('Actor time value object', () => { const actorTimeVO = new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 06:45'), - firstMaxDatetime: new Date('2023-09-01 07:15'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 06:45'), - lastMaxDatetime: new Date('2024-08-30 07:15'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }); expect(actorTimeVO.role).toBe(Role.DRIVER); expect(actorTimeVO.target).toBe(Target.START); - expect(actorTimeVO.firstDatetime.getHours()).toBe(7); - expect(actorTimeVO.firstMinDatetime.getMinutes()).toBe(45); - expect(actorTimeVO.firstMaxDatetime.getMinutes()).toBe(15); - expect(actorTimeVO.lastDatetime.getHours()).toBe(7); - expect(actorTimeVO.lastMinDatetime.getMinutes()).toBe(45); - expect(actorTimeVO.lastMaxDatetime.getMinutes()).toBe(15); + expect(actorTimeVO.firstDatetime.getUTCHours()).toBe(7); + expect(actorTimeVO.firstMinDatetime.getUTCMinutes()).toBe(45); + expect(actorTimeVO.firstMaxDatetime.getUTCMinutes()).toBe(15); + expect(actorTimeVO.lastDatetime.getUTCHours()).toBe(7); + expect(actorTimeVO.lastMinDatetime.getUTCMinutes()).toBe(45); + expect(actorTimeVO.lastMaxDatetime.getUTCMinutes()).toBe(15); }); it('should throw an error if dates are inconsistent', () => { expect( @@ -30,12 +30,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 07:05'), - firstMaxDatetime: new Date('2023-09-01 07:15'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 06:45'), - lastMaxDatetime: new Date('2024-08-30 07:15'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T07:05Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -43,12 +43,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 06:45'), - firstMaxDatetime: new Date('2023-09-01 06:55'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 06:45'), - lastMaxDatetime: new Date('2024-08-30 07:15'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T06:55Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -56,12 +56,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 06:45'), - firstMaxDatetime: new Date('2023-09-01 07:15'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 07:05'), - lastMaxDatetime: new Date('2024-08-30 07:15'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T07:05Z'), + lastMaxDatetime: new Date('2024-08-30T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -69,12 +69,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 06:45'), - firstMaxDatetime: new Date('2023-09-01 07:15'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 06:45'), - lastMaxDatetime: new Date('2024-08-30 06:35'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-30T07:00Z'), + lastMinDatetime: new Date('2024-08-30T06:45Z'), + lastMaxDatetime: new Date('2024-08-30T06:35Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -82,12 +82,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2024-08-30 07:00'), - firstMinDatetime: new Date('2024-08-30 06:45'), - firstMaxDatetime: new Date('2024-08-30 07:15'), - lastDatetime: new Date('2023-09-01 07:00'), - lastMinDatetime: new Date('2023-09-01 06:45'), - lastMaxDatetime: new Date('2023-09-01 07:15'), + firstDatetime: new Date('2024-08-30T07:00Z'), + firstMinDatetime: new Date('2024-08-30T06:45Z'), + firstMaxDatetime: new Date('2024-08-30T07:15Z'), + lastDatetime: new Date('2023-09-01T07:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45Z'), + lastMaxDatetime: new Date('2023-09-01T07:15Z'), }), ).toThrow(ArgumentInvalidException); expect( @@ -95,12 +95,12 @@ describe('Actor time value object', () => { new ActorTime({ role: Role.DRIVER, target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 06:45'), - firstMaxDatetime: new Date('2023-09-01 07:15'), - lastDatetime: new Date('2024-08-31 07:00'), - lastMinDatetime: new Date('2024-08-31 06:45'), - lastMaxDatetime: new Date('2024-08-31 06:35'), + firstDatetime: new Date('2023-09-01T07:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45Z'), + firstMaxDatetime: new Date('2023-09-01T07:15Z'), + lastDatetime: new Date('2024-08-31T07:00Z'), + lastMinDatetime: new Date('2024-08-31T06:45Z'), + lastMaxDatetime: new Date('2024-08-31T06:35Z'), }), ).toThrow(ArgumentInvalidException); }); diff --git a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts index c9757ce..720d5b6 100644 --- a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts +++ b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts @@ -67,6 +67,10 @@ class SomeSelector extends Selector { CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, diff --git a/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts index a31ff87..9dccb04 100644 --- a/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts +++ b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts @@ -6,78 +6,175 @@ import { describe('Calendar tools service', () => { describe('First date', () => { it('should return the first date for a given week day within a date range', () => { - const firstDate: Date = CalendarTools.firstDate( - 1, - '2023-08-31', - '2023-09-07', - ); - expect(firstDate.getDay()).toBe(1); - expect(firstDate.getDate()).toBe(4); - expect(firstDate.getMonth()).toBe(8); - const secondDate: Date = CalendarTools.firstDate( - 5, - '2023-08-31', - '2023-09-07', - ); - expect(secondDate.getDay()).toBe(5); - expect(secondDate.getDate()).toBe(1); - expect(secondDate.getMonth()).toBe(8); - const thirdDate: Date = CalendarTools.firstDate( - 4, - '2023-08-31', - '2023-09-07', - ); - expect(thirdDate.getDay()).toBe(4); - expect(thirdDate.getDate()).toBe(31); - expect(thirdDate.getMonth()).toBe(7); + const firstDate: Date = CalendarTools.firstDate(1, { + lowerDate: '2023-08-31', + higherDate: '2023-09-07', + }); + expect(firstDate.getUTCDay()).toBe(1); + expect(firstDate.getUTCDate()).toBe(4); + expect(firstDate.getUTCMonth()).toBe(8); + const secondDate: Date = CalendarTools.firstDate(5, { + lowerDate: '2023-08-31', + higherDate: '2023-09-07', + }); + expect(secondDate.getUTCDay()).toBe(5); + expect(secondDate.getUTCDate()).toBe(1); + expect(secondDate.getUTCMonth()).toBe(8); + const thirdDate: Date = CalendarTools.firstDate(4, { + lowerDate: '2023-08-31', + higherDate: '2023-09-07', + }); + expect(thirdDate.getUTCDay()).toBe(4); + expect(thirdDate.getUTCDate()).toBe(31); + expect(thirdDate.getUTCMonth()).toBe(7); }); it('should throw an exception if a given week day is not within a date range', () => { expect(() => { - CalendarTools.firstDate(1, '2023-09-05', '2023-09-07'); + CalendarTools.firstDate(1, { + lowerDate: '2023-09-05', + higherDate: '2023-09-07', + }); }).toThrow(CalendarToolsException); }); it('should throw an exception if a given week day is invalid', () => { expect(() => { - CalendarTools.firstDate(8, '2023-09-05', '2023-09-07'); + CalendarTools.firstDate(8, { + lowerDate: '2023-09-05', + higherDate: '2023-09-07', + }); }).toThrow(CalendarToolsException); }); }); describe('Second date', () => { it('should return the last date for a given week day within a date range', () => { - const firstDate: Date = CalendarTools.lastDate( - 0, - '2023-09-30', - '2024-09-30', - ); - expect(firstDate.getDay()).toBe(0); - expect(firstDate.getDate()).toBe(29); - expect(firstDate.getMonth()).toBe(8); - const secondDate: Date = CalendarTools.lastDate( - 5, - '2023-09-30', - '2024-09-30', - ); - expect(secondDate.getDay()).toBe(5); - expect(secondDate.getDate()).toBe(27); - expect(secondDate.getMonth()).toBe(8); - const thirdDate: Date = CalendarTools.lastDate( - 1, - '2023-09-30', - '2024-09-30', - ); - expect(thirdDate.getDay()).toBe(1); - expect(thirdDate.getDate()).toBe(30); - expect(thirdDate.getMonth()).toBe(8); + const firstDate: Date = CalendarTools.lastDate(0, { + lowerDate: '2023-09-30', + higherDate: '2024-09-30', + }); + expect(firstDate.getUTCDay()).toBe(0); + expect(firstDate.getUTCDate()).toBe(29); + expect(firstDate.getUTCMonth()).toBe(8); + const secondDate: Date = CalendarTools.lastDate(5, { + lowerDate: '2023-09-30', + higherDate: '2024-09-30', + }); + expect(secondDate.getUTCDay()).toBe(5); + expect(secondDate.getUTCDate()).toBe(27); + expect(secondDate.getUTCMonth()).toBe(8); + const thirdDate: Date = CalendarTools.lastDate(1, { + lowerDate: '2023-09-30', + higherDate: '2024-09-30', + }); + expect(thirdDate.getUTCDay()).toBe(1); + expect(thirdDate.getUTCDate()).toBe(30); + expect(thirdDate.getUTCMonth()).toBe(8); }); it('should throw an exception if a given week day is not within a date range', () => { expect(() => { - CalendarTools.lastDate(2, '2024-09-27', '2024-09-30'); + CalendarTools.lastDate(2, { + lowerDate: '2024-09-27', + higherDate: '2024-09-30', + }); }).toThrow(CalendarToolsException); }); it('should throw an exception if a given week day is invalid', () => { expect(() => { - CalendarTools.lastDate(8, '2023-09-30', '2024-09-30'); + CalendarTools.lastDate(8, { + lowerDate: '2023-09-30', + higherDate: '2024-09-30', + }); + }).toThrow(CalendarToolsException); + }); + }); + + describe('Datetime from string', () => { + it('should return a date with time from a string without additional seconds', () => { + const datetime: Date = CalendarTools.datetimeFromString( + '2023-09-01', + '07:12', + ); + expect(datetime.getUTCMinutes()).toBe(12); + }); + it('should return a date with time from a string with additional seconds', () => { + const datetime: Date = CalendarTools.datetimeFromString( + '2023-09-01', + '07:12', + 60, + ); + expect(datetime.getUTCMinutes()).toBe(13); + }); + it('should return a date with time from a string with negative additional seconds', () => { + const datetime: Date = CalendarTools.datetimeFromString( + '2023-09-01', + '07:00', + -60, + ); + console.log(datetime); + expect(datetime.getUTCHours()).toBe(6); + expect(datetime.getUTCMinutes()).toBe(59); + }); + }); + + describe('epochDaysFromTime', () => { + it('should return the epoch day for day 1', () => { + const days: Date[] = CalendarTools.epochDaysFromTime(1, '07:00'); + expect(days).toHaveLength(1); + expect(days[0].getUTCFullYear()).toBe(1969); + expect(days[0].getUTCMonth()).toBe(11); + expect(days[0].getUTCDate()).toBe(29); + }); + it('should return the epoch day for day 2', () => { + const days: Date[] = CalendarTools.epochDaysFromTime(2, '07:00'); + expect(days).toHaveLength(1); + expect(days[0].getUTCFullYear()).toBe(1969); + expect(days[0].getUTCMonth()).toBe(11); + expect(days[0].getUTCDate()).toBe(30); + }); + it('should return the epoch day for day 3', () => { + const days: Date[] = CalendarTools.epochDaysFromTime(3, '07:00'); + expect(days).toHaveLength(1); + expect(days[0].getUTCFullYear()).toBe(1969); + expect(days[0].getUTCMonth()).toBe(11); + expect(days[0].getUTCDate()).toBe(31); + }); + it('should return the epoch day for day 4', () => { + const days: Date[] = CalendarTools.epochDaysFromTime(4, '07:00'); + expect(days).toHaveLength(1); + expect(days[0].getUTCFullYear()).toBe(1970); + expect(days[0].getUTCMonth()).toBe(0); + expect(days[0].getUTCDate()).toBe(1); + }); + it('should return the epoch day for day 5', () => { + const days: Date[] = CalendarTools.epochDaysFromTime(5, '07:00'); + expect(days).toHaveLength(1); + expect(days[0].getUTCFullYear()).toBe(1970); + expect(days[0].getUTCMonth()).toBe(0); + expect(days[0].getUTCDate()).toBe(2); + }); + it('should return the epoch days for day 0', () => { + const days: Date[] = CalendarTools.epochDaysFromTime(0, '07:00'); + expect(days).toHaveLength(2); + expect(days[0].getUTCFullYear()).toBe(1969); + expect(days[0].getUTCMonth()).toBe(11); + expect(days[0].getUTCDate()).toBe(28); + expect(days[1].getUTCFullYear()).toBe(1970); + expect(days[1].getUTCMonth()).toBe(0); + expect(days[1].getUTCDate()).toBe(4); + }); + it('should return the epoch days for day 6', () => { + const days: Date[] = CalendarTools.epochDaysFromTime(6, '07:00'); + expect(days).toHaveLength(2); + expect(days[0].getUTCFullYear()).toBe(1969); + expect(days[0].getUTCMonth()).toBe(11); + expect(days[0].getUTCDate()).toBe(27); + expect(days[1].getUTCFullYear()).toBe(1970); + expect(days[1].getUTCMonth()).toBe(0); + expect(days[1].getUTCDate()).toBe(3); + }); + it('should throw an exception if a given week day is invalid', () => { + expect(() => { + CalendarTools.epochDaysFromTime(8, '07:00'); }).toThrow(CalendarToolsException); }); }); diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts index c70d573..9556e10 100644 --- a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -2,13 +2,16 @@ import { Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; -import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; describe('Candidate entity', () => { it('should create a new candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, @@ -52,10 +55,15 @@ describe('Candidate entity', () => { }); expect(candidateEntity.id.length).toBe(36); }); + it('should set a candidate entity carpool path', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.689445, @@ -98,10 +106,8 @@ describe('Candidate entity', () => { }, }).setCarpoolPath([ { - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -114,10 +120,8 @@ describe('Candidate entity', () => { ], }, { - point: new Point({ - lat: 48.8566, - lon: 2.3522, - }), + lat: 48.8566, + lon: 2.3522, actors: [ new Actor({ role: Role.DRIVER, @@ -130,12 +134,17 @@ describe('Candidate entity', () => { ], }, ]); - expect(candidateEntity.getProps().carpoolSteps).toHaveLength(2); + expect(candidateEntity.getProps().carpoolPath).toHaveLength(2); }); + it('should create a new candidate entity with spacetime metrics', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, @@ -180,98 +189,113 @@ describe('Candidate entity', () => { expect(candidateEntity.getProps().distance).toBe(352688); expect(candidateEntity.getProps().duration).toBe(14587); }); - it('should not validate a candidate entity with exceeding distance detour', () => { - const candidateEntity: CandidateEntity = CandidateEntity.create({ - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - role: Role.DRIVER, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, + + describe('detour validation', () => { + it('should not validate a candidate entity with exceeding distance detour', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', }, - { - lat: 48.84877, - lon: 2.398457, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, }, - ], - passengerWaypoints: [ - { - lat: 48.849445, - lon: 6.68651, + }).setMetrics(458690, 13980); + expect(candidateEntity.isDetourValid()).toBeFalsy(); + }); + it('should not validate a candidate entity with exceeding duration detour', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', }, - { - lat: 47.18746, - lon: 2.89742, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, }, - ], - driverDistance: 350145, - driverDuration: 13548, - driverSchedule: [ - { - day: 0, - time: '07:00', - margin: 900, - }, - ], - passengerSchedule: [ - { - day: 0, - time: '07:10', - margin: 900, - }, - ], - spacetimeDetourRatio: { - maxDistanceDetourRatio: 0.3, - maxDurationDetourRatio: 0.3, - }, - }).setMetrics(458690, 13980); - expect(candidateEntity.isDetourValid()).toBeFalsy(); + }).setMetrics(352368, 18314); + expect(candidateEntity.isDetourValid()).toBeFalsy(); + }); }); - it('should not validate a candidate entity with exceeding duration detour', () => { - const candidateEntity: CandidateEntity = CandidateEntity.create({ - id: 'cc260669-1c6d-441f-80a5-19cd59afb777', - role: Role.DRIVER, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, - }, - { - lat: 48.84877, - lon: 2.398457, - }, - ], - passengerWaypoints: [ - { - lat: 48.849445, - lon: 6.68651, - }, - { - lat: 47.18746, - lon: 2.89742, - }, - ], - driverDistance: 350145, - driverDuration: 13548, - driverSchedule: [ - { - day: 0, - time: '07:00', - margin: 900, - }, - ], - passengerSchedule: [ - { - day: 0, - time: '07:10', - margin: 900, - }, - ], - spacetimeDetourRatio: { - maxDistanceDetourRatio: 0.3, - maxDurationDetourRatio: 0.3, - }, - }).setMetrics(352368, 18314); - expect(candidateEntity.isDetourValid()).toBeFalsy(); + + describe('Journeys', () => { + it('should create journeys', () => {}); }); }); diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index 8fbb643..709c2bf 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -1,7 +1,7 @@ import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service'; import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors'; import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; -import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object'; +import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object'; const waypoint1: Point = new Point({ lat: 0, @@ -34,71 +34,71 @@ describe('Carpool Path Creator Service', () => { [waypoint1, waypoint6], [waypoint2, waypoint5], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(4); - expect(carpoolSteps[0].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(4); + expect(carpoolPath[0].actors.length).toBe(1); }); it('should create a simple carpool path with same destination for driver and passenger', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint2, waypoint6], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(3); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(2); - expect(carpoolSteps[2].actors.length).toBe(2); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(3); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].actors.length).toBe(2); + expect(carpoolPath[2].actors.length).toBe(2); }); it('should create a simple carpool path with same waypoints for driver and passenger', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint6], [waypoint1, waypoint6], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(2); - expect(carpoolSteps[0].actors.length).toBe(2); - expect(carpoolSteps[1].actors.length).toBe(2); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(2); + expect(carpoolPath[0].actors.length).toBe(2); + expect(carpoolPath[1].actors.length).toBe(2); }); it('should create a complex carpool path with 3 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint6], [waypoint2, waypoint5], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(5); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(2); - expect(carpoolSteps[2].actors.length).toBe(1); - expect(carpoolSteps[3].actors.length).toBe(2); - expect(carpoolSteps[4].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(5); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].actors.length).toBe(2); + expect(carpoolPath[2].actors.length).toBe(1); + expect(carpoolPath[3].actors.length).toBe(2); + expect(carpoolPath[4].actors.length).toBe(1); }); it('should create a complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint3, waypoint4, waypoint6], [waypoint2, waypoint5], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(6); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(2); - expect(carpoolSteps[2].actors.length).toBe(1); - expect(carpoolSteps[3].actors.length).toBe(1); - expect(carpoolSteps[4].actors.length).toBe(2); - expect(carpoolSteps[5].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(6); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].actors.length).toBe(2); + expect(carpoolPath[2].actors.length).toBe(1); + expect(carpoolPath[3].actors.length).toBe(1); + expect(carpoolPath[4].actors.length).toBe(2); + expect(carpoolPath[5].actors.length).toBe(1); }); it('should create a alternate complex carpool path with 4 driver waypoints', () => { const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator( [waypoint1, waypoint2, waypoint5, waypoint6], [waypoint3, waypoint4], ); - const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath(); - expect(carpoolSteps).toHaveLength(6); - expect(carpoolSteps[0].actors.length).toBe(1); - expect(carpoolSteps[1].actors.length).toBe(1); - expect(carpoolSteps[2].actors.length).toBe(2); - expect(carpoolSteps[3].actors.length).toBe(2); - expect(carpoolSteps[4].actors.length).toBe(1); - expect(carpoolSteps[5].actors.length).toBe(1); + const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath(); + expect(carpoolPath).toHaveLength(6); + expect(carpoolPath[0].actors.length).toBe(1); + expect(carpoolPath[1].actors.length).toBe(1); + expect(carpoolPath[2].actors.length).toBe(2); + expect(carpoolPath[3].actors.length).toBe(2); + expect(carpoolPath[4].actors.length).toBe(1); + expect(carpoolPath[5].actors.length).toBe(1); }); it('should throw an exception if less than 2 driver waypoints are given', () => { expect(() => { diff --git a/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-item.value-object.spec.ts similarity index 60% rename from src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts rename to src/modules/ad/tests/unit/core/carpool-path-item.value-object.spec.ts index d633079..9da4378 100644 --- a/src/modules/ad/tests/unit/core/carpool-step.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-item.value-object.spec.ts @@ -2,16 +2,13 @@ import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library'; import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; -import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; -import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object'; +import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object'; -describe('CarpoolStep value object', () => { - it('should create a carpoolStep value object', () => { - const carpoolStepVO = new CarpoolStep({ - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), +describe('Carpool Path Item value object', () => { + it('should create a path item value object', () => { + const carpoolPathItemVO = new CarpoolPathItem({ + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -23,28 +20,24 @@ describe('CarpoolStep value object', () => { }), ], }); - expect(carpoolStepVO.point.lon).toBe(6.17651); - expect(carpoolStepVO.point.lat).toBe(48.689445); - expect(carpoolStepVO.actors).toHaveLength(2); + expect(carpoolPathItemVO.lon).toBe(6.17651); + expect(carpoolPathItemVO.lat).toBe(48.689445); + expect(carpoolPathItemVO.actors).toHaveLength(2); }); it('should throw an exception if actors is empty', () => { expect(() => { - new CarpoolStep({ - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + new CarpoolPathItem({ + lat: 48.689445, + lon: 6.17651, actors: [], }); }).toThrow(ArgumentOutOfRangeException); }); it('should throw an exception if actors contains more than one driver', () => { expect(() => { - new CarpoolStep({ - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + new CarpoolPathItem({ + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, diff --git a/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts b/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts index c4461c1..5c4322e 100644 --- a/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/journey-item.value-object.spec.ts @@ -28,7 +28,9 @@ describe('Journey item value object', () => { expect(journeyItemVO.distance).toBe(48754); expect(journeyItemVO.lon).toBe(6.17651); expect(journeyItemVO.lat).toBe(48.689445); - expect(journeyItemVO.actorTimes[0].firstMaxDatetime.getMinutes()).toBe(15); + expect(journeyItemVO.actorTimes[0].firstMaxDatetime.getUTCMinutes()).toBe( + 15, + ); }); it('should throw an error if actorTimes is too short', () => { expect( diff --git a/src/modules/ad/tests/unit/core/journey.completer.spec.ts b/src/modules/ad/tests/unit/core/journey.completer.spec.ts index de6dfd9..ee9d348 100644 --- a/src/modules/ad/tests/unit/core/journey.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.completer.spec.ts @@ -6,7 +6,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; -import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; const originWaypoint: Waypoint = { position: 0, @@ -66,6 +65,10 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, @@ -108,10 +111,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ }, }).setCarpoolPath([ { - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -124,10 +125,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ ], }, { - point: new Point({ - lat: 48.8566, - lon: 2.3522, - }), + lat: 48.8566, + lon: 2.3522, actors: [ new Actor({ role: Role.DRIVER, diff --git a/src/modules/ad/tests/unit/core/journey.value-object.spec.ts b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts index 3d321e5..6bc1c5e 100644 --- a/src/modules/ad/tests/unit/core/journey.value-object.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.value-object.spec.ts @@ -1,7 +1,4 @@ -import { - ArgumentInvalidException, - ArgumentOutOfRangeException, -} from '@mobicoop/ddd-library'; +import { ArgumentInvalidException } from '@mobicoop/ddd-library'; import { Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; @@ -13,7 +10,6 @@ describe('Journey value object', () => { const journeyVO = new Journey({ firstDate: new Date('2023-09-01'), lastDate: new Date('2024-08-30'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, @@ -109,114 +105,9 @@ describe('Journey value object', () => { }), ], }); - expect(journeyVO.day).toBe(5); expect(journeyVO.journeyItems).toHaveLength(4); - expect(journeyVO.firstDate.getDate()).toBe(1); - expect(journeyVO.lastDate.getMonth()).toBe(7); - }); - it('should throw an error if day is wrong', () => { - expect( - () => - new Journey({ - firstDate: new Date('2023-09-01'), - lastDate: new Date('2024-08-30'), - day: 7, - journeyItems: [ - new JourneyItem({ - lat: 48.689445, - lon: 6.17651, - duration: 0, - distance: 0, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 06:45'), - firstMaxDatetime: new Date('2023-09-01 07:15'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 06:45'), - lastMaxDatetime: new Date('2024-08-30 07:15'), - }), - ], - }), - new JourneyItem({ - lat: 48.369445, - lon: 6.67487, - duration: 2100, - distance: 56878, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.NEUTRAL, - firstDatetime: new Date('2023-09-01 07:35'), - firstMinDatetime: new Date('2023-09-01 07:20'), - firstMaxDatetime: new Date('2023-09-01 07:50'), - lastDatetime: new Date('2024-08-30 07:35'), - lastMinDatetime: new Date('2024-08-30 07:20'), - lastMaxDatetime: new Date('2024-08-30 07:50'), - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.START, - firstDatetime: new Date('2023-09-01 07:32'), - firstMinDatetime: new Date('2023-09-01 07:17'), - firstMaxDatetime: new Date('2023-09-01 07:47'), - lastDatetime: new Date('2024-08-30 07:32'), - lastMinDatetime: new Date('2024-08-30 07:17'), - lastMaxDatetime: new Date('2024-08-30 07:47'), - }), - ], - }), - new JourneyItem({ - lat: 47.98487, - lon: 6.9427, - duration: 3840, - distance: 76491, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.NEUTRAL, - firstDatetime: new Date('2023-09-01 08:04'), - firstMinDatetime: new Date('2023-09-01 07:51'), - firstMaxDatetime: new Date('2023-09-01 08:19'), - lastDatetime: new Date('2024-08-30 08:04'), - lastMinDatetime: new Date('2024-08-30 07:51'), - lastMaxDatetime: new Date('2024-08-30 08:19'), - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.FINISH, - firstDatetime: new Date('2023-09-01 08:01'), - firstMinDatetime: new Date('2023-09-01 07:46'), - firstMaxDatetime: new Date('2023-09-01 08:16'), - lastDatetime: new Date('2024-08-30 08:01'), - lastMinDatetime: new Date('2024-08-30 07:46'), - lastMaxDatetime: new Date('2024-08-30 08:16'), - }), - ], - }), - new JourneyItem({ - lat: 47.365987, - lon: 7.02154, - duration: 4980, - distance: 96475, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.FINISH, - firstDatetime: new Date('2023-09-01 08:23'), - firstMinDatetime: new Date('2023-09-01 08:08'), - firstMaxDatetime: new Date('2023-09-01 08:38'), - lastDatetime: new Date('2024-08-30 08:23'), - lastMinDatetime: new Date('2024-08-30 08:08'), - lastMaxDatetime: new Date('2024-08-30 08:38'), - }), - ], - }), - ], - }), - ).toThrow(ArgumentOutOfRangeException); + expect(journeyVO.firstDate.getUTCDate()).toBe(1); + expect(journeyVO.lastDate.getUTCMonth()).toBe(7); }); it('should throw an error if dates are inconsistent', () => { expect( @@ -224,7 +115,6 @@ describe('Journey value object', () => { new Journey({ firstDate: new Date('2023-09-01'), lastDate: new Date('2024-08-31'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, @@ -326,7 +216,6 @@ describe('Journey value object', () => { new Journey({ firstDate: new Date('2024-08-30'), lastDate: new Date('2023-09-01'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, @@ -430,7 +319,6 @@ describe('Journey value object', () => { new Journey({ firstDate: new Date('2023-09-01'), lastDate: new Date('2024-08-30'), - day: 5, journeyItems: [ new JourneyItem({ lat: 48.689445, diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index a7f0a90..7a33fa1 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -50,6 +50,10 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, @@ -94,6 +98,10 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.689445, diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 1155369..8bb2344 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -49,6 +49,10 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index 80291da..8daaf47 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -9,7 +9,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; -import { Point } from '@modules/ad/core/domain/value-objects/point.value-object'; const originWaypoint: Waypoint = { position: 0, @@ -70,6 +69,10 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, driverWaypoints: [ { lat: 48.678454, @@ -112,10 +115,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ }, }).setCarpoolPath([ { - point: new Point({ - lat: 48.689445, - lon: 6.17651, - }), + lat: 48.689445, + lon: 6.17651, actors: [ new Actor({ role: Role.DRIVER, @@ -128,10 +129,8 @@ const candidate: CandidateEntity = CandidateEntity.create({ ], }, { - point: new Point({ - lat: 48.8566, - lon: 2.3522, - }), + lat: 48.8566, + lon: 2.3522, actors: [ new Actor({ role: Role.DRIVER, diff --git a/src/modules/geography/infrastructure/graphhopper-georouter.ts b/src/modules/geography/infrastructure/graphhopper-georouter.ts index a12b526..635ac3f 100644 --- a/src/modules/geography/infrastructure/graphhopper-georouter.ts +++ b/src/modules/geography/infrastructure/graphhopper-georouter.ts @@ -164,12 +164,14 @@ export class GraphhopperGeorouter implements GeorouterPort { points: [[number, number]], snappedWaypoints: [[number, number]], ): number[] => { - const indices = snappedWaypoints.map((waypoint) => - points.findIndex( - (point) => point[0] == waypoint[0] && point[1] == waypoint[1], - ), + const indices: number[] = snappedWaypoints.map( + (waypoint: [number, number]) => + points.findIndex( + (point) => point[0] == waypoint[0] && point[1] == waypoint[1], + ), ); - if (indices.find((index) => index == -1) === undefined) return indices; + if (indices.find((index: number) => index == -1) === undefined) + return indices; const missedWaypoints = indices .map( (value, index) => From d8df086c6d609cd04e9bf761cae02998efc3b44c Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 25 Sep 2023 11:42:22 +0200 Subject: [PATCH 43/52] compute journeys with tests --- .../queries/match/algorithm.abstract.ts | 2 +- .../ad/core/domain/calendar-tools.service.ts | 12 +- .../ad/core/domain/candidate.entity.ts | 54 +- .../unit/core/calendar-tools.service.spec.ts | 13 +- .../tests/unit/core/candidate.entity.spec.ts | 588 +++++++++++------- 5 files changed, 422 insertions(+), 247 deletions(-) diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index 6915366..faeb9bc 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -20,7 +20,7 @@ export abstract class Algorithm { for (const processor of this.processors) { this.candidates = await processor.execute(this.candidates); } - console.log(JSON.stringify(this.candidates, null, 2)); + // console.log(JSON.stringify(this.candidates, null, 2)); return this.candidates.map((candidate: CandidateEntity) => MatchEntity.create({ adId: candidate.id }), ); diff --git a/src/modules/ad/core/domain/calendar-tools.service.ts b/src/modules/ad/core/domain/calendar-tools.service.ts index 184adc6..933b628 100644 --- a/src/modules/ad/core/domain/calendar-tools.service.ts +++ b/src/modules/ad/core/domain/calendar-tools.service.ts @@ -63,14 +63,16 @@ export class CalendarTools { }; /** - * Returns a date from a date and time as strings, adding optional seconds + * Returns a date from a date (as a date) and a time (as a string), adding optional seconds */ - static datetimeFromString = ( - date: string, + static datetimeWithSeconds = ( + date: Date, time: string, additionalSeconds = 0, ): Date => { - const datetime = new Date(`${date}T${time}:00Z`); + const datetime: Date = new Date(date); + datetime.setUTCHours(parseInt(time.split(':')[0])); + datetime.setUTCMinutes(parseInt(time.split(':')[1])); datetime.setUTCSeconds(additionalSeconds); return datetime; }; @@ -79,7 +81,7 @@ export class CalendarTools { * Returns dates from a day and time based on unix epoch day * (1970-01-01 is day 4) * The method returns an array of dates because for edges (day 0 and 6) - * we need to return 2 possibilities + * we need to return 2 possibilities : one for the previous week, one for the next week */ static epochDaysFromTime = (weekDay: number, time: string): Date[] => { if (weekDay < 0 || weekDay > 6) diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 073dcea..a61369e 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -46,6 +46,7 @@ export class CandidateEntity extends AggregateRoot { /** * Create the journeys based on the driver schedule (the driver 'drives' the carpool !) + * This is a tedious process : additional information can be found in deeper methods ! */ createJourneys = (): CandidateEntity => { this.props.journeys = this.props.driverSchedule.map( @@ -90,6 +91,13 @@ export class CandidateEntity extends AggregateRoot { this._createJourneyItem(carpoolPathItem, index, driverScheduleItem), ) as JourneyItem[]; + /** + * Create a journey item based on a carpool path item and driver schedule item + * The stepIndex is used to get the duration to reach the carpool path item + * from the steps prop (computed previously by a georouter) + * There MUST be a one/one relation between the carpool path items indexes + * and the steps indexes. + */ private _createJourneyItem = ( carpoolPathItem: CarpoolPathItem, stepIndex: number, @@ -123,42 +131,55 @@ export class CandidateEntity extends AggregateRoot { actor.target == Target.START ? 0 : duration; + const firstDate: Date = CalendarTools.firstDate( + scheduleItem.day, + this.props.dateInterval, + ); + const lastDate: Date = CalendarTools.lastDate( + scheduleItem.day, + this.props.dateInterval, + ); return new ActorTime({ role: actor.role, target: actor.target, - firstDatetime: CalendarTools.datetimeFromString( - this.props.dateInterval.lowerDate, + firstDatetime: CalendarTools.datetimeWithSeconds( + firstDate, scheduleItem.time, effectiveDuration, ), - firstMinDatetime: CalendarTools.datetimeFromString( - this.props.dateInterval.lowerDate, + firstMinDatetime: CalendarTools.datetimeWithSeconds( + firstDate, scheduleItem.time, -scheduleItem.margin + effectiveDuration, ), - firstMaxDatetime: CalendarTools.datetimeFromString( - this.props.dateInterval.lowerDate, + firstMaxDatetime: CalendarTools.datetimeWithSeconds( + firstDate, scheduleItem.time, scheduleItem.margin + effectiveDuration, ), - lastDatetime: CalendarTools.datetimeFromString( - this.props.dateInterval.higherDate, + lastDatetime: CalendarTools.datetimeWithSeconds( + lastDate, scheduleItem.time, effectiveDuration, ), - lastMinDatetime: CalendarTools.datetimeFromString( - this.props.dateInterval.higherDate, + lastMinDatetime: CalendarTools.datetimeWithSeconds( + lastDate, scheduleItem.time, -scheduleItem.margin + effectiveDuration, ), - lastMaxDatetime: CalendarTools.datetimeFromString( - this.props.dateInterval.higherDate, + lastMaxDatetime: CalendarTools.datetimeWithSeconds( + lastDate, scheduleItem.time, scheduleItem.margin + effectiveDuration, ), }); }; + /** + * Get the closest (in time) passenger schedule item for a given driver schedule item + * This is mandatory as we can't rely only on the day of the schedule item : + * items on different days can match when playing with margins around midnight + */ private _closestPassengerScheduleItem = ( driverScheduleItem: ScheduleItem, ): ScheduleItem => @@ -179,8 +200,12 @@ export class CandidateEntity extends AggregateRoot { : currentScheduleItemGap, ).scheduleItem; + /** + * Find the passenger schedule item with the minimum duration between a given date and the dates of the passenger schedule + */ private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap => this.props.passengerSchedule + // first map the passenger schedule to "real" dates (we use unix epoch date as base) .map( (scheduleItem: ScheduleItem) => { @@ -191,16 +216,21 @@ export class CandidateEntity extends AggregateRoot { ), }, ) + // then compute the duration in seconds to the given date + // for each "real" date computed in step 1 .map((scheduleItemRange: ScheduleItemRange) => ({ scheduleItem: scheduleItemRange.scheduleItem, gap: scheduleItemRange.range + // compute the duration .map((scheduleDate: Date) => Math.round(Math.abs(scheduleDate.getTime() - date.getTime())), ) + // keep the lowest duration .reduce((previousGap: number, currentGap: number) => previousGap < currentGap ? previousGap : currentGap, ), })) + // finally, keep the passenger schedule item with the lowest duration .reduce( ( previousScheduleItemGap: ScheduleItemGap, diff --git a/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts index 9dccb04..67fcf7b 100644 --- a/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts +++ b/src/modules/ad/tests/unit/core/calendar-tools.service.spec.ts @@ -90,27 +90,26 @@ describe('Calendar tools service', () => { describe('Datetime from string', () => { it('should return a date with time from a string without additional seconds', () => { - const datetime: Date = CalendarTools.datetimeFromString( - '2023-09-01', + const datetime: Date = CalendarTools.datetimeWithSeconds( + new Date('2023-09-01'), '07:12', ); expect(datetime.getUTCMinutes()).toBe(12); }); it('should return a date with time from a string with additional seconds', () => { - const datetime: Date = CalendarTools.datetimeFromString( - '2023-09-01', + const datetime: Date = CalendarTools.datetimeWithSeconds( + new Date('2023-09-01'), '07:12', 60, ); expect(datetime.getUTCMinutes()).toBe(13); }); it('should return a date with time from a string with negative additional seconds', () => { - const datetime: Date = CalendarTools.datetimeFromString( - '2023-09-01', + const datetime: Date = CalendarTools.datetimeWithSeconds( + new Date('2023-09-01'), '07:00', -60, ); - console.log(datetime); expect(datetime.getUTCHours()).toBe(6); expect(datetime.getUTCMinutes()).toBe(59); }); diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts index 9556e10..bcc7961 100644 --- a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -1,7 +1,244 @@ import { Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; -import { Target } from '@modules/ad/core/domain/candidate.types'; +import { + SpacetimeDetourRatio, + Target, +} from '@modules/ad/core/domain/candidate.types'; import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object'; +import { CarpoolPathItemProps } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object'; +import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; +import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object'; +import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object'; +import { StepProps } from '@modules/ad/core/domain/value-objects/step.value-object'; + +const waypointsSet1: PointProps[] = [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, +]; + +const waypointsSet2: PointProps[] = [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, +]; + +const schedule1: ScheduleItemProps[] = [ + { + day: 1, + time: '07:00', + margin: 900, + }, +]; + +const schedule2: ScheduleItemProps[] = [ + { + day: 1, + time: '07:10', + margin: 900, + }, +]; + +const schedule3: ScheduleItemProps[] = [ + { + day: 1, + time: '06:30', + margin: 900, + }, + { + day: 2, + time: '06:30', + margin: 900, + }, + { + day: 3, + time: '06:00', + margin: 900, + }, + { + day: 4, + time: '06:30', + margin: 900, + }, + { + day: 5, + time: '06:30', + margin: 900, + }, +]; + +const schedule4: ScheduleItemProps[] = [ + { + day: 1, + time: '06:50', + margin: 900, + }, + { + day: 2, + time: '06:50', + margin: 900, + }, + { + day: 4, + time: '06:50', + margin: 900, + }, + { + day: 5, + time: '06:50', + margin: 900, + }, +]; + +const schedule5: ScheduleItemProps[] = [ + { + day: 0, + time: '00:10', + margin: 900, + }, + { + day: 1, + time: '07:05', + margin: 900, + }, +]; + +const schedule6: ScheduleItemProps[] = [ + { + day: 1, + time: '23:10', + margin: 900, + }, + { + day: 6, + time: '23:45', + margin: 900, + }, +]; + +const spacetimeDetourRatio: SpacetimeDetourRatio = { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, +}; + +const carpoolPath1: CarpoolPathItemProps[] = [ + { + lat: 48.689445, + lon: 6.17651, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.START, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.START, + }), + ], + }, + { + lat: 48.8566, + lon: 2.3522, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.FINISH, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.FINISH, + }), + ], + }, +]; + +const carpoolPath2: CarpoolPathItemProps[] = [ + { + lat: 48.689445, + lon: 6.17651, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.START, + }), + ], + }, + { + lat: 48.678451, + lon: 6.168784, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.NEUTRAL, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.START, + }), + ], + }, + { + lat: 48.848715, + lon: 2.36985, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.NEUTRAL, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.FINISH, + }), + ], + }, + { + lat: 48.8566, + lon: 2.3522, + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.FINISH, + }), + ], + }, +]; + +const steps: StepProps[] = [ + { + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + }, + { + lat: 48.678451, + lon: 6.168784, + duration: 1254, + distance: 33462, + }, + { + lat: 48.848715, + lon: 2.36985, + duration: 12477, + distance: 343654, + }, + { + lat: 48.8566, + lon: 2.3522, + duration: 13548, + distance: 350145, + }, +]; describe('Candidate entity', () => { it('should create a new candidate entity', () => { @@ -12,46 +249,13 @@ describe('Candidate entity', () => { lowerDate: '2023-08-28', higherDate: '2023-08-28', }, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, - }, - { - lat: 48.84877, - lon: 2.398457, - }, - ], - passengerWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, driverDistance: 350145, driverDuration: 13548, - driverSchedule: [ - { - day: 0, - time: '07:00', - margin: 900, - }, - ], - passengerSchedule: [ - { - day: 0, - time: '07:10', - margin: 900, - }, - ], - spacetimeDetourRatio: { - maxDistanceDetourRatio: 0.3, - maxDurationDetourRatio: 0.3, - }, + driverSchedule: schedule1, + passengerSchedule: schedule2, + spacetimeDetourRatio, }); expect(candidateEntity.id.length).toBe(36); }); @@ -64,76 +268,14 @@ describe('Candidate entity', () => { lowerDate: '2023-08-28', higherDate: '2023-08-28', }, - driverWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], - passengerWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], + driverWaypoints: waypointsSet2, + passengerWaypoints: waypointsSet2, driverDistance: 350145, driverDuration: 13548, - driverSchedule: [ - { - day: 0, - time: '07:00', - margin: 900, - }, - ], - passengerSchedule: [ - { - day: 0, - time: '07:10', - margin: 900, - }, - ], - spacetimeDetourRatio: { - maxDistanceDetourRatio: 0.3, - maxDurationDetourRatio: 0.3, - }, - }).setCarpoolPath([ - { - lat: 48.689445, - lon: 6.17651, - actors: [ - new Actor({ - role: Role.DRIVER, - target: Target.START, - }), - new Actor({ - role: Role.PASSENGER, - target: Target.START, - }), - ], - }, - { - lat: 48.8566, - lon: 2.3522, - actors: [ - new Actor({ - role: Role.DRIVER, - target: Target.FINISH, - }), - new Actor({ - role: Role.PASSENGER, - target: Target.FINISH, - }), - ], - }, - ]); + driverSchedule: schedule1, + passengerSchedule: schedule2, + spacetimeDetourRatio, + }).setCarpoolPath(carpoolPath1); expect(candidateEntity.getProps().carpoolPath).toHaveLength(2); }); @@ -145,46 +287,13 @@ describe('Candidate entity', () => { lowerDate: '2023-08-28', higherDate: '2023-08-28', }, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, - }, - { - lat: 48.84877, - lon: 2.398457, - }, - ], - passengerWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, driverDistance: 350145, driverDuration: 13548, - driverSchedule: [ - { - day: 0, - time: '07:00', - margin: 900, - }, - ], - passengerSchedule: [ - { - day: 0, - time: '07:10', - margin: 900, - }, - ], - spacetimeDetourRatio: { - maxDistanceDetourRatio: 0.3, - maxDurationDetourRatio: 0.3, - }, + driverSchedule: schedule1, + passengerSchedule: schedule2, + spacetimeDetourRatio, }).setMetrics(352688, 14587); expect(candidateEntity.getProps().distance).toBe(352688); expect(candidateEntity.getProps().duration).toBe(14587); @@ -199,46 +308,13 @@ describe('Candidate entity', () => { lowerDate: '2023-08-28', higherDate: '2023-08-28', }, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, - }, - { - lat: 48.84877, - lon: 2.398457, - }, - ], - passengerWaypoints: [ - { - lat: 48.849445, - lon: 6.68651, - }, - { - lat: 47.18746, - lon: 2.89742, - }, - ], + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, driverDistance: 350145, driverDuration: 13548, - driverSchedule: [ - { - day: 0, - time: '07:00', - margin: 900, - }, - ], - passengerSchedule: [ - { - day: 0, - time: '07:10', - margin: 900, - }, - ], - spacetimeDetourRatio: { - maxDistanceDetourRatio: 0.3, - maxDurationDetourRatio: 0.3, - }, + driverSchedule: schedule1, + passengerSchedule: schedule2, + spacetimeDetourRatio, }).setMetrics(458690, 13980); expect(candidateEntity.isDetourValid()).toBeFalsy(); }); @@ -250,52 +326,120 @@ describe('Candidate entity', () => { lowerDate: '2023-08-28', higherDate: '2023-08-28', }, - driverWaypoints: [ - { - lat: 48.678454, - lon: 6.189745, - }, - { - lat: 48.84877, - lon: 2.398457, - }, - ], - passengerWaypoints: [ - { - lat: 48.849445, - lon: 6.68651, - }, - { - lat: 47.18746, - lon: 2.89742, - }, - ], + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, driverDistance: 350145, driverDuration: 13548, - driverSchedule: [ - { - day: 0, - time: '07:00', - margin: 900, - }, - ], - passengerSchedule: [ - { - day: 0, - time: '07:10', - margin: 900, - }, - ], - spacetimeDetourRatio: { - maxDistanceDetourRatio: 0.3, - maxDurationDetourRatio: 0.3, - }, + driverSchedule: schedule1, + passengerSchedule: schedule2, + spacetimeDetourRatio, }).setMetrics(352368, 18314); expect(candidateEntity.isDetourValid()).toBeFalsy(); }); }); describe('Journeys', () => { - it('should create journeys', () => {}); + it('should create journeys for a single date', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: schedule1, + passengerSchedule: schedule2, + spacetimeDetourRatio, + }) + .setCarpoolPath(carpoolPath2) + .setSteps(steps) + .createJourneys(); + expect(candidateEntity.getProps().journeys).toHaveLength(1); + }); + it('should create journeys for multiple dates', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-09-01', + higherDate: '2024-09-01', + }, + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: schedule3, + passengerSchedule: schedule4, + spacetimeDetourRatio, + }) + .setCarpoolPath(carpoolPath2) + .setSteps(steps) + .createJourneys(); + expect(candidateEntity.getProps().journeys).toHaveLength(5); + expect( + ( + candidateEntity.getProps().journeys as Journey[] + )[0].firstDate.getDate(), + ).toBe(4); + expect( + ( + candidateEntity.getProps().journeys as Journey[] + )[0].journeyItems[1].actorTimes[1].firstMinDatetime.getDate(), + ).toBe(4); + expect( + ( + candidateEntity.getProps().journeys as Journey[] + )[1].journeyItems[1].actorTimes[1].firstMinDatetime.getDate(), + ).toBe(5); + }); + it('should create journeys for multiple dates, including week edges (saturday/sunday)', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-09-01', + higherDate: '2024-09-01', + }, + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: schedule5, + passengerSchedule: schedule6, + spacetimeDetourRatio, + }) + .setCarpoolPath(carpoolPath2) + .setSteps(steps) + .createJourneys(); + expect(candidateEntity.getProps().journeys).toHaveLength(2); + expect( + (candidateEntity.getProps().journeys as Journey[])[0].journeyItems[1] + .actorTimes[0].target, + ).toBe(Target.NEUTRAL); + expect( + ( + candidateEntity.getProps().journeys as Journey[] + )[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCHours(), + ).toBe(0); + expect( + ( + candidateEntity.getProps().journeys as Journey[] + )[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(), + ).toBe(30); + expect( + ( + candidateEntity.getProps().journeys as Journey[] + )[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCHours(), + ).toBe(23); + expect( + ( + candidateEntity.getProps().journeys as Journey[] + )[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCMinutes(), + ).toBe(30); + }); }); }); From eafa3c8bddd6cae39e2bc8fc208685d53b14e4e3 Mon Sep 17 00:00:00 2001 From: sbriat Date: Mon, 25 Sep 2023 16:03:37 +0200 Subject: [PATCH 44/52] empty journey filter --- .../queries/match/filter/journey.filter.ts | 10 ++ .../match/passenger-oriented-algorithm.ts | 2 + .../ad/core/domain/candidate.entity.ts | 13 +- .../value-objects/journey.value-object.ts | 34 +++++ .../tests/unit/core/candidate.entity.spec.ts | 64 +++++++++- .../ad/tests/unit/core/journey.filter.spec.ts | 117 ++++++++++++++++++ 6 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 src/modules/ad/core/application/queries/match/filter/journey.filter.ts create mode 100644 src/modules/ad/tests/unit/core/journey.filter.spec.ts diff --git a/src/modules/ad/core/application/queries/match/filter/journey.filter.ts b/src/modules/ad/core/application/queries/match/filter/journey.filter.ts new file mode 100644 index 0000000..bcb9c09 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/filter/journey.filter.ts @@ -0,0 +1,10 @@ +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; +import { Filter } from './filter.abstract'; + +/** + * Filter candidates with empty journeys + */ +export class JourneyFilter extends Filter { + filter = async (candidates: CandidateEntity[]): Promise => + candidates.filter((candidate: CandidateEntity) => candidate.hasJourneys()); +} diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts index 6baa3b4..8621d6f 100644 --- a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -9,6 +9,7 @@ import { RouteCompleterType, } from './completer/route.completer'; import { JourneyCompleter } from './completer/journey.completer'; +import { JourneyFilter } from './filter/journey.filter'; export class PassengerOrientedAlgorithm extends Algorithm { constructor( @@ -23,6 +24,7 @@ export class PassengerOrientedAlgorithm extends Algorithm { new PassengerOrientedGeoFilter(query), new RouteCompleter(query, RouteCompleterType.DETAILED), new JourneyCompleter(query), + new JourneyFilter(query), ]; } } diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index a61369e..ec3a7c9 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -44,15 +44,22 @@ export class CandidateEntity extends AggregateRoot { isDetourValid = (): boolean => this._validateDistanceDetour() && this._validateDurationDetour(); + hasJourneys = (): boolean => + this.getProps().journeys !== undefined && + (this.getProps().journeys as Journey[]).length > 0; + /** * Create the journeys based on the driver schedule (the driver 'drives' the carpool !) * This is a tedious process : additional information can be found in deeper methods ! */ createJourneys = (): CandidateEntity => { - this.props.journeys = this.props.driverSchedule.map( - (driverScheduleItem: ScheduleItem) => + this.props.journeys = this.props.driverSchedule + // first we create the journeys + .map((driverScheduleItem: ScheduleItem) => this._createJourney(driverScheduleItem), - ); + ) + // then we filter the ones with invalid pickups + .filter((journey: Journey) => journey.hasValidPickUp()); return this; }; diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts index df03132..4b6a0e6 100644 --- a/src/modules/ad/core/domain/value-objects/journey.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -1,5 +1,8 @@ import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; import { JourneyItem } from './journey-item.value-object'; +import { ActorTime } from './actor-time.value-object'; +import { Role } from '../ad.types'; +import { Target } from '../candidate.types'; /** Note: * Value Objects with multiple properties can contain @@ -25,6 +28,37 @@ export class Journey extends ValueObject { return this.props.journeyItems; } + hasValidPickUp = (): boolean => { + const passengerDepartureJourneyItem: JourneyItem = this.journeyItems.find( + (journeyItem: JourneyItem) => + journeyItem.actorTimes.find( + (actorTime: ActorTime) => + actorTime.role == Role.PASSENGER && + actorTime.target == Target.START, + ) as ActorTime, + ) as JourneyItem; + const passengerDepartureActorTime = + passengerDepartureJourneyItem.actorTimes.find( + (actorTime: ActorTime) => + actorTime.role == Role.PASSENGER && actorTime.target == Target.START, + ) as ActorTime; + const driverNeutralActorTime = + passengerDepartureJourneyItem.actorTimes.find( + (actorTime: ActorTime) => + actorTime.role == Role.DRIVER && actorTime.target == Target.NEUTRAL, + ) as ActorTime; + return ( + (passengerDepartureActorTime.firstMinDatetime <= + driverNeutralActorTime.firstMaxDatetime && + driverNeutralActorTime.firstMaxDatetime <= + passengerDepartureActorTime.firstMaxDatetime) || + (passengerDepartureActorTime.firstMinDatetime <= + driverNeutralActorTime.firstMinDatetime && + driverNeutralActorTime.firstMinDatetime <= + passengerDepartureActorTime.firstMaxDatetime) + ); + }; + protected validate(props: JourneyProps): void { if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay()) throw new ArgumentInvalidException( diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts index bcc7961..f1e4b7d 100644 --- a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -103,7 +103,7 @@ const schedule4: ScheduleItemProps[] = [ const schedule5: ScheduleItemProps[] = [ { day: 0, - time: '00:10', + time: '00:02', margin: 900, }, { @@ -121,7 +121,15 @@ const schedule6: ScheduleItemProps[] = [ }, { day: 6, - time: '23:45', + time: '23:57', + margin: 900, + }, +]; + +const schedule7: ScheduleItemProps[] = [ + { + day: 4, + time: '19:00', margin: 900, }, ]; @@ -379,7 +387,7 @@ describe('Candidate entity', () => { .setCarpoolPath(carpoolPath2) .setSteps(steps) .createJourneys(); - expect(candidateEntity.getProps().journeys).toHaveLength(5); + expect(candidateEntity.getProps().journeys).toHaveLength(4); expect( ( candidateEntity.getProps().journeys as Journey[] @@ -415,7 +423,7 @@ describe('Candidate entity', () => { .setCarpoolPath(carpoolPath2) .setSteps(steps) .createJourneys(); - expect(candidateEntity.getProps().journeys).toHaveLength(2); + expect(candidateEntity.getProps().journeys).toHaveLength(1); expect( (candidateEntity.getProps().journeys as Journey[])[0].journeyItems[1] .actorTimes[0].target, @@ -429,7 +437,7 @@ describe('Candidate entity', () => { ( candidateEntity.getProps().journeys as Journey[] )[0].journeyItems[1].actorTimes[0].firstDatetime.getUTCMinutes(), - ).toBe(30); + ).toBe(22); expect( ( candidateEntity.getProps().journeys as Journey[] @@ -439,7 +447,51 @@ describe('Candidate entity', () => { ( candidateEntity.getProps().journeys as Journey[] )[0].journeyItems[1].actorTimes[1].firstMinDatetime.getUTCMinutes(), - ).toBe(30); + ).toBe(42); + }); + + it('should not create journeys if dates does not match', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-09-01', + higherDate: '2024-09-01', + }, + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: schedule1, + passengerSchedule: schedule7, + spacetimeDetourRatio, + }) + .setCarpoolPath(carpoolPath2) + .setSteps(steps) + .createJourneys(); + expect(candidateEntity.getProps().journeys).toHaveLength(0); + expect(candidateEntity.hasJourneys()).toBeFalsy(); + }); + + it('should not verify journeys if journeys is undefined', () => { + const candidateEntity: CandidateEntity = CandidateEntity.create({ + id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', + role: Role.PASSENGER, + dateInterval: { + lowerDate: '2023-09-01', + higherDate: '2024-09-01', + }, + driverWaypoints: waypointsSet1, + passengerWaypoints: waypointsSet2, + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: schedule1, + passengerSchedule: schedule7, + spacetimeDetourRatio, + }) + .setCarpoolPath(carpoolPath2) + .setSteps(steps); + expect(candidateEntity.hasJourneys()).toBeFalsy(); }); }); }); diff --git a/src/modules/ad/tests/unit/core/journey.filter.spec.ts b/src/modules/ad/tests/unit/core/journey.filter.spec.ts new file mode 100644 index 0000000..8f707c1 --- /dev/null +++ b/src/modules/ad/tests/unit/core/journey.filter.spec.ts @@ -0,0 +1,117 @@ +import { JourneyFilter } from '@modules/ad/core/application/queries/match/filter/journey.filter'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: true, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + { + getBasic: jest.fn(), + getDetailed: jest.fn(), + }, +); + +const candidate: CandidateEntity = CandidateEntity.create({ + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + role: Role.DRIVER, + dateInterval: { + lowerDate: '2023-08-28', + higherDate: '2023-08-28', + }, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + driverSchedule: [ + { + day: 0, + time: '07:00', + margin: 900, + }, + ], + passengerSchedule: [ + { + day: 0, + time: '07:10', + margin: 900, + }, + ], + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, +}); + +describe('Passenger oriented time filter', () => { + it('should not filter valid candidates', async () => { + const passengerOrientedTimeFilter: JourneyFilter = new JourneyFilter( + matchQuery, + ); + candidate.hasJourneys = () => true; + const filteredCandidates: CandidateEntity[] = + await passengerOrientedTimeFilter.filter([candidate]); + expect(filteredCandidates.length).toBe(1); + }); + it('should filter invalid candidates', async () => { + const passengerOrientedTimeFilter: JourneyFilter = new JourneyFilter( + matchQuery, + ); + candidate.hasJourneys = () => false; + const filteredCandidates: CandidateEntity[] = + await passengerOrientedTimeFilter.filter([candidate]); + expect(filteredCandidates.length).toBe(0); + }); +}); From 528ecfb3f9f1c1684d983e65788e0a7892d74397 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 26 Sep 2023 09:51:26 +0200 Subject: [PATCH 45/52] first working e2e version --- .../queries/match/algorithm.abstract.ts | 12 +- src/modules/ad/core/domain/match.types.ts | 10 ++ .../ad/interface/dtos/actor.response.dto.ts | 10 ++ .../ad/interface/dtos/journey.response.dto.ts | 7 ++ .../ad/interface/dtos/match.response.dto.ts | 5 + .../ad/interface/dtos/step.response.dto.ts | 9 ++ .../grpc-controllers/match.grpc-controller.ts | 26 ++++ .../interface/grpc-controllers/matcher.proto | 29 +++++ .../ad/tests/unit/core/match.entity.spec.ts | 107 ++++++++++++++++ .../interface/match.grpc.controller.spec.ts | 115 +++++++++++++++++- 10 files changed, 323 insertions(+), 7 deletions(-) create mode 100644 src/modules/ad/interface/dtos/actor.response.dto.ts create mode 100644 src/modules/ad/interface/dtos/journey.response.dto.ts create mode 100644 src/modules/ad/interface/dtos/step.response.dto.ts diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index faeb9bc..a31a7f5 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -2,6 +2,10 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { MatchEntity } from '../../../domain/match.entity'; import { MatchQuery } from './match.query'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; +import { + Journey, + JourneyProps, +} from '@modules/ad/core/domain/value-objects/journey.value-object'; export abstract class Algorithm { protected candidates: CandidateEntity[]; @@ -22,7 +26,13 @@ export abstract class Algorithm { } // console.log(JSON.stringify(this.candidates, null, 2)); return this.candidates.map((candidate: CandidateEntity) => - MatchEntity.create({ adId: candidate.id }), + MatchEntity.create({ + adId: candidate.id, + role: candidate.getProps().role, + distance: candidate.getProps().distance as number, + duration: candidate.getProps().duration as number, + journeys: candidate.getProps().journeys as Journey[], + }), ); }; } diff --git a/src/modules/ad/core/domain/match.types.ts b/src/modules/ad/core/domain/match.types.ts index 911029b..055bce3 100644 --- a/src/modules/ad/core/domain/match.types.ts +++ b/src/modules/ad/core/domain/match.types.ts @@ -1,13 +1,23 @@ import { AlgorithmType } from '../application/types/algorithm.types'; +import { Role } from './ad.types'; +import { JourneyProps } from './value-objects/journey.value-object'; // All properties that a Match has export interface MatchProps { adId: string; + role: Role; + distance: number; + duration: number; + journeys: JourneyProps[]; } // Properties that are needed for a Match creation export interface CreateMatchProps { adId: string; + role: Role; + distance: number; + duration: number; + journeys: JourneyProps[]; } export interface DefaultMatchQueryProps { diff --git a/src/modules/ad/interface/dtos/actor.response.dto.ts b/src/modules/ad/interface/dtos/actor.response.dto.ts new file mode 100644 index 0000000..bb86ff3 --- /dev/null +++ b/src/modules/ad/interface/dtos/actor.response.dto.ts @@ -0,0 +1,10 @@ +export class ActorResponseDto { + role: string; + target: string; + firstDatetime: string; + firstMinDatetime: string; + firstMaxDatetime: string; + lastDatetime: string; + lastMinDatetime: string; + lastMaxDatetime: string; +} diff --git a/src/modules/ad/interface/dtos/journey.response.dto.ts b/src/modules/ad/interface/dtos/journey.response.dto.ts new file mode 100644 index 0000000..f4c5736 --- /dev/null +++ b/src/modules/ad/interface/dtos/journey.response.dto.ts @@ -0,0 +1,7 @@ +import { StepResponseDto } from './step.response.dto'; + +export class JourneyResponseDto { + firstDate: string; + lastDate: string; + steps: StepResponseDto[]; +} diff --git a/src/modules/ad/interface/dtos/match.response.dto.ts b/src/modules/ad/interface/dtos/match.response.dto.ts index bc01e6f..ce22d4b 100644 --- a/src/modules/ad/interface/dtos/match.response.dto.ts +++ b/src/modules/ad/interface/dtos/match.response.dto.ts @@ -1,5 +1,10 @@ import { ResponseBase } from '@mobicoop/ddd-library'; +import { JourneyResponseDto } from './journey.response.dto'; export class MatchResponseDto extends ResponseBase { adId: string; + role: string; + distance: number; + duration: number; + journeys: JourneyResponseDto[]; } diff --git a/src/modules/ad/interface/dtos/step.response.dto.ts b/src/modules/ad/interface/dtos/step.response.dto.ts new file mode 100644 index 0000000..b2240ac --- /dev/null +++ b/src/modules/ad/interface/dtos/step.response.dto.ts @@ -0,0 +1,9 @@ +import { ActorResponseDto } from './actor.response.dto'; + +export class StepResponseDto { + duration: number; + distance: number; + lon: number; + lat: number; + actors: ActorResponseDto[]; +} diff --git a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index 1f80b69..e21c784 100644 --- a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -9,6 +9,9 @@ import { MatchQuery } from '@modules/ad/core/application/queries/match/match.que import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; +import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; @UsePipes( new RpcValidationPipe({ @@ -34,6 +37,29 @@ export class MatchGrpcController { data: matches.map((match: MatchEntity) => ({ ...new ResponseBase(match), adId: match.getProps().adId, + role: match.getProps().role, + distance: match.getProps().distance, + duration: match.getProps().duration, + journeys: match.getProps().journeys.map((journey: Journey) => ({ + firstDate: journey.firstDate.toUTCString(), + lastDate: journey.lastDate.toUTCString(), + steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ + duration: journeyItem.duration, + distance: journeyItem.distance as number, + lon: journeyItem.lon, + lat: journeyItem.lat, + actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ + role: actorTime.role, + target: actorTime.target, + firstDatetime: actorTime.firstMinDatetime.toUTCString(), + firstMinDatetime: actorTime.firstMinDatetime.toUTCString(), + firstMaxDatetime: actorTime.firstMaxDatetime.toUTCString(), + lastDatetime: actorTime.lastDatetime.toUTCString(), + lastMinDatetime: actorTime.lastMinDatetime.toUTCString(), + lastMaxDatetime: actorTime.lastMaxDatetime.toUTCString(), + })), + })), + })), })), page: 1, perPage: 5, diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index 11d9a4d..29ac5ff 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -56,6 +56,35 @@ enum AlgorithmType { message Match { string id = 1; string adId = 2; + string role = 3; + int32 duration = 4; + int32 distance = 5; + repeated Journey journeys = 6; +} + +message Journey { + string firstDate = 1; + string lastDate = 2; + repeated Step steps = 3; +} + +message Step { + int32 duration = 1; + int32 distance = 2; + double lon = 3; + double lat = 4; + repeated Actor actors = 5; +} + +message Actor { + string role = 1; + string target = 2; + string firstDatetime = 3; + string firstMinDatetime = 4; + string firstMaxDatetime = 5; + string lastDatetime = 6; + string lastMinDatetime = 7; + string lastMaxDatetime = 8; } message Matches { diff --git a/src/modules/ad/tests/unit/core/match.entity.spec.ts b/src/modules/ad/tests/unit/core/match.entity.spec.ts index 5f2f4e5..fe57511 100644 --- a/src/modules/ad/tests/unit/core/match.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/match.entity.spec.ts @@ -1,9 +1,116 @@ +import { Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; describe('Match entity create', () => { it('should create a new match entity', async () => { const match: MatchEntity = MatchEntity.create({ adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + role: Role.DRIVER, + distance: 356041, + duration: 12647, + journeys: [ + { + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }, + ], }); expect(match.id.length).toBe(36); }); diff --git a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 6f75433..9d39955 100644 --- a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -2,8 +2,11 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; -import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller'; @@ -53,10 +56,110 @@ const mockQueryBus = { .fn() .mockImplementationOnce(() => [ MatchEntity.create({ - adId: '0cc87f3b-7a27-4eff-9850-a5d642c2a0c3', - }), - MatchEntity.create({ - adId: 'e4cc156f-aaa5-4270-bf6f-82f5a230d748', + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + role: Role.DRIVER, + distance: 356041, + duration: 12647, + journeys: [ + { + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }, + ], }), ]) .mockImplementationOnce(() => { @@ -103,7 +206,7 @@ describe('Match Grpc Controller', () => { const matchPaginatedResponseDto = await matchGrpcController.match( punctualMatchRequestDto, ); - expect(matchPaginatedResponseDto.data).toHaveLength(2); + expect(matchPaginatedResponseDto.data).toHaveLength(1); expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); }); From d0285e265e3fa8582ddad0de92cfefbc291d61b2 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 26 Sep 2023 14:03:34 +0200 Subject: [PATCH 46/52] format response --- .env.dist | 2 +- src/modules/ad/ad.di-tokens.ts | 3 + src/modules/ad/ad.module.ts | 9 +- .../queries/match/algorithm.abstract.ts | 8 +- .../selector/passenger-oriented.selector.ts | 1 + src/modules/ad/core/domain/candidate.types.ts | 4 +- src/modules/ad/core/domain/match.entity.ts | 12 +- src/modules/ad/core/domain/match.types.ts | 12 +- .../carpool-path-item.value-object.ts | 6 +- .../journey-item.value-object.ts | 19 +- .../value-objects/journey.value-object.ts | 38 ++- .../output-datetime-transformer.ts | 116 ++++++++ .../ad/interface/dtos/actor.response.dto.ts | 6 - .../ad/interface/dtos/journey.response.dto.ts | 1 + .../ad/interface/dtos/match.response.dto.ts | 7 + .../ad/interface/dtos/step.response.dto.ts | 3 +- .../grpc-controllers/match.grpc-controller.ts | 37 +-- .../interface/grpc-controllers/matcher.proto | 32 +- src/modules/ad/match.mapper.ts | 78 +++++ .../unit/core/algorithm.abstract.spec.ts | 1 + .../tests/unit/core/candidate.entity.spec.ts | 12 +- .../tests/unit/core/journey.completer.spec.ts | 1 + .../ad/tests/unit/core/journey.filter.spec.ts | 1 + .../ad/tests/unit/core/match.entity.spec.ts | 5 +- ...er-oriented-carpool-path-completer.spec.ts | 2 + .../passenger-oriented-geo-filter.spec.ts | 1 + .../tests/unit/core/route.completer.spec.ts | 1 + .../output-datetime-transformer.spec.ts | 280 ++++++++++++++++++ .../interface/match.grpc.controller.spec.ts | 130 +++++++- .../ad/tests/unit/match.mapper.spec.ts | 154 ++++++++++ 30 files changed, 903 insertions(+), 79 deletions(-) create mode 100644 src/modules/ad/infrastructure/output-datetime-transformer.ts create mode 100644 src/modules/ad/match.mapper.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts create mode 100644 src/modules/ad/tests/unit/match.mapper.spec.ts diff --git a/.env.dist b/.env.dist index 8f1ee3f..c4859fc 100644 --- a/.env.dist +++ b/.env.dist @@ -62,4 +62,4 @@ SEATS_REQUESTED=1 STRICT_FREQUENCY=false # default timezone -DEFAULT_TIMEZONE=Europe/Paris +TIMEZONE=Europe/Paris diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 87c11ec..e225a20 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -12,3 +12,6 @@ export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER'); +export const OUTPUT_DATETIME_TRANSFORMER = Symbol( + 'OUTPUT_DATETIME_TRANSFORMER', +); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index bebd374..90348ac 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -11,6 +11,7 @@ import { TIME_CONVERTER, INPUT_DATETIME_TRANSFORMER, AD_GET_DETAILED_ROUTE_CONTROLLER, + OUTPUT_DATETIME_TRANSFORMER, } from './ad.di-tokens'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { AdRepository } from './infrastructure/ad.repository'; @@ -29,6 +30,8 @@ import { TimezoneFinder } from './infrastructure/timezone-finder'; import { TimeConverter } from './infrastructure/time-converter'; import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller'; +import { MatchMapper } from './match.mapper'; +import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; const grpcControllers = [MatchGrpcController]; @@ -38,7 +41,7 @@ const commandHandlers: Provider[] = [CreateAdService]; const queryHandlers: Provider[] = [MatchQueryHandler]; -const mappers: Provider[] = [AdMapper]; +const mappers: Provider[] = [AdMapper, MatchMapper]; const repositories: Provider[] = [ { @@ -89,6 +92,10 @@ const adapters: Provider[] = [ provide: INPUT_DATETIME_TRANSFORMER, useClass: InputDateTimeTransformer, }, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useClass: OutputDateTimeTransformer, + }, ]; @Module({ diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index a31a7f5..ac902fc 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -2,10 +2,7 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { MatchEntity } from '../../../domain/match.entity'; import { MatchQuery } from './match.query'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; -import { - Journey, - JourneyProps, -} from '@modules/ad/core/domain/value-objects/journey.value-object'; +import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; export abstract class Algorithm { protected candidates: CandidateEntity[]; @@ -29,8 +26,11 @@ export abstract class Algorithm { MatchEntity.create({ adId: candidate.id, role: candidate.getProps().role, + frequency: candidate.getProps().frequency, distance: candidate.getProps().distance as number, duration: candidate.getProps().duration as number, + initialDistance: candidate.getProps().driverDistance, + initialDuration: candidate.getProps().driverDuration, journeys: candidate.getProps().journeys as Journey[], }), ); diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index ebb1cbd..013dd7e 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -36,6 +36,7 @@ export class PassengerOrientedSelector extends Selector { CandidateEntity.create({ id: adEntity.id, role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER, + frequency: adEntity.getProps().frequency, dateInterval: { lowerDate: this._maxDateString( this.query.fromDate, diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index c9bc237..a7d82cf 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,4 +1,4 @@ -import { Role } from './ad.types'; +import { Frequency, Role } from './ad.types'; import { PointProps } from './value-objects/point.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { CarpoolPathItemProps } from './value-objects/carpool-path-item.value-object'; @@ -8,6 +8,7 @@ import { StepProps } from './value-objects/step.value-object'; // All properties that a Candidate has export interface CandidateProps { role: Role; + frequency: Frequency; driverWaypoints: PointProps[]; passengerWaypoints: PointProps[]; driverSchedule: ScheduleItemProps[]; @@ -27,6 +28,7 @@ export interface CandidateProps { export interface CreateCandidateProps { id: string; role: Role; + frequency: Frequency; driverDistance: number; driverDuration: number; driverWaypoints: PointProps[]; diff --git a/src/modules/ad/core/domain/match.entity.ts b/src/modules/ad/core/domain/match.entity.ts index 520fe28..1a1e5ea 100644 --- a/src/modules/ad/core/domain/match.entity.ts +++ b/src/modules/ad/core/domain/match.entity.ts @@ -7,7 +7,17 @@ export class MatchEntity extends AggregateRoot { static create = (create: CreateMatchProps): MatchEntity => { const id = v4(); - const props: MatchProps = { ...create }; + const props: MatchProps = { + ...create, + distanceDetour: create.distance - create.initialDistance, + durationDetour: create.duration - create.initialDuration, + distanceDetourPercentage: parseFloat( + ((100 * create.distance) / create.initialDistance - 100).toFixed(2), + ), + durationDetourPercentage: parseFloat( + ((100 * create.duration) / create.initialDuration - 100).toFixed(2), + ), + }; return new MatchEntity({ id, props }); }; diff --git a/src/modules/ad/core/domain/match.types.ts b/src/modules/ad/core/domain/match.types.ts index 055bce3..66539fb 100644 --- a/src/modules/ad/core/domain/match.types.ts +++ b/src/modules/ad/core/domain/match.types.ts @@ -1,13 +1,20 @@ import { AlgorithmType } from '../application/types/algorithm.types'; -import { Role } from './ad.types'; +import { Frequency, Role } from './ad.types'; import { JourneyProps } from './value-objects/journey.value-object'; // All properties that a Match has export interface MatchProps { adId: string; role: Role; + frequency: Frequency; distance: number; duration: number; + initialDistance: number; + initialDuration: number; + distanceDetour: number; + durationDetour: number; + distanceDetourPercentage: number; + durationDetourPercentage: number; journeys: JourneyProps[]; } @@ -15,8 +22,11 @@ export interface MatchProps { export interface CreateMatchProps { adId: string; role: Role; + frequency: Frequency; distance: number; duration: number; + initialDistance: number; + initialDuration: number; journeys: JourneyProps[]; } diff --git a/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts index bb7dafa..377000a 100644 --- a/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts @@ -2,7 +2,7 @@ import { ArgumentOutOfRangeException, ValueObject, } from '@mobicoop/ddd-library'; -import { Actor } from './actor.value-object'; +import { Actor, ActorProps } from './actor.value-object'; import { Role } from '../ad.types'; import { Point, PointProps } from './point.value-object'; @@ -12,7 +12,7 @@ import { Point, PointProps } from './point.value-object'; * */ export interface CarpoolPathItemProps extends PointProps { - actors: Actor[]; + actors: ActorProps[]; } export class CarpoolPathItem extends ValueObject { @@ -24,7 +24,7 @@ export class CarpoolPathItem extends ValueObject { return this.props.lat; } - get actors(): Actor[] { + get actors(): ActorProps[] { return this.props.actors; } diff --git a/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts b/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts index 4f1a9d4..ef0c9fd 100644 --- a/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts @@ -2,8 +2,9 @@ import { ArgumentOutOfRangeException, ValueObject, } from '@mobicoop/ddd-library'; -import { ActorTime } from './actor-time.value-object'; +import { ActorTime, ActorTimeProps } from './actor-time.value-object'; import { Step, StepProps } from './step.value-object'; +import { Role } from '../ad.types'; /** Note: * Value Objects with multiple properties can contain @@ -11,7 +12,7 @@ import { Step, StepProps } from './step.value-object'; * */ export interface JourneyItemProps extends StepProps { - actorTimes: ActorTime[]; + actorTimes: ActorTimeProps[]; } export class JourneyItem extends ValueObject { @@ -31,10 +32,22 @@ export class JourneyItem extends ValueObject { return this.props.lat; } - get actorTimes(): ActorTime[] { + get actorTimes(): ActorTimeProps[] { return this.props.actorTimes; } + driverTime = (): string => { + const driverTime: Date = ( + this.actorTimes.find( + (actorTime: ActorTime) => actorTime.role == Role.DRIVER, + ) as ActorTime + ).firstDatetime; + return `${driverTime.getHours().toString().padStart(2, '0')}:${driverTime + .getMinutes() + .toString() + .padStart(2, '0')}`; + }; + protected validate(props: JourneyItemProps): void { // validate step props new Step({ diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts index 4b6a0e6..d36bc9e 100644 --- a/src/modules/ad/core/domain/value-objects/journey.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -1,8 +1,9 @@ import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; -import { JourneyItem } from './journey-item.value-object'; +import { JourneyItem, JourneyItemProps } from './journey-item.value-object'; import { ActorTime } from './actor-time.value-object'; import { Role } from '../ad.types'; import { Target } from '../candidate.types'; +import { Point } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain @@ -12,7 +13,7 @@ import { Target } from '../candidate.types'; export interface JourneyProps { firstDate: Date; lastDate: Date; - journeyItems: JourneyItem[]; + journeyItems: JourneyItemProps[]; } export class Journey extends ValueObject { @@ -24,7 +25,7 @@ export class Journey extends ValueObject { return this.props.lastDate; } - get journeyItems(): JourneyItem[] { + get journeyItems(): JourneyItemProps[] { return this.props.journeyItems; } @@ -59,6 +60,37 @@ export class Journey extends ValueObject { ); }; + firstDriverDepartureTime = (): string => { + const firstDriverDepartureDatetime: Date = ( + this._driverDepartureJourneyItem().actorTimes.find( + (actorTime: ActorTime) => + actorTime.role == Role.DRIVER && actorTime.target == Target.START, + ) as ActorTime + ).firstDatetime; + return `${firstDriverDepartureDatetime + .getUTCHours() + .toString() + .padStart(2, '0')}:${firstDriverDepartureDatetime + .getUTCMinutes() + .toString() + .padStart(2, '0')}`; + }; + + driverOrigin = (): Point => + new Point({ + lon: this._driverDepartureJourneyItem().lon, + lat: this._driverDepartureJourneyItem().lat, + }); + + private _driverDepartureJourneyItem = (): JourneyItem => + this.journeyItems.find( + (journeyItem: JourneyItem) => + journeyItem.actorTimes.find( + (actorTime: ActorTime) => + actorTime.role == Role.DRIVER && actorTime.target == Target.START, + ) as ActorTime, + ) as JourneyItem; + protected validate(props: JourneyProps): void { if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay()) throw new ArgumentInvalidException( diff --git a/src/modules/ad/infrastructure/output-datetime-transformer.ts b/src/modules/ad/infrastructure/output-datetime-transformer.ts new file mode 100644 index 0000000..d2d44be --- /dev/null +++ b/src/modules/ad/infrastructure/output-datetime-transformer.ts @@ -0,0 +1,116 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + DateTimeTransformerPort, + Frequency, + GeoDateTime, +} from '../core/application/ports/datetime-transformer.port'; +import { TimeConverterPort } from '../core/application/ports/time-converter.port'; +import { TIMEZONE_FINDER, TIME_CONVERTER } from '../ad.di-tokens'; +import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; + +@Injectable() +export class OutputDateTimeTransformer implements DateTimeTransformerPort { + constructor( + @Inject(TIMEZONE_FINDER) + private readonly timezoneFinder: TimezoneFinderPort, + @Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort, + ) {} + + /** + * Compute the fromDate : if an ad is punctual, the departure date + * is converted from UTC to the local date with the time and timezone + */ + fromDate = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) return geoFromDate.date; + return this.timeConverter + .utcStringDateTimeToLocalIsoString( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ) + .split('T')[0]; + }; + + /** + * Get the toDate depending on frequency, time and timezone : + * if the ad is punctual, the toDate is equal to the fromDate + */ + toDate = ( + toDate: string, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): string => { + if (frequency === Frequency.RECURRENT) return toDate; + return this.fromDate(geoFromDate, frequency); + }; + + /** + * Get the day for a schedule item : + * - if the ad is punctual, the day is infered from fromDate + * - if the ad is recurrent, the day is computed by converting the time from utc to local time + */ + day = ( + day: number, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): number => { + if (frequency === Frequency.RECURRENT) + return this.recurrentDay( + day, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ); + return new Date(this.fromDate(geoFromDate, frequency)).getDay(); + }; + + /** + * Get the utc time + */ + time = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) + return this.timeConverter.utcStringTimeToLocalStringTime( + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ); + return this.timeConverter + .utcStringDateTimeToLocalIsoString( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ) + .split('T')[1] + .split(':', 2) + .join(':'); + }; + + /** + * Get the day for a schedule item for a recurrent ad + * The day may change when transforming from utc to local timezone + */ + private recurrentDay = ( + day: number, + time: string, + timezone: string, + ): number => { + const unixEpochDay = 4; // 1970-01-01 is a thursday ! + const localBaseDay = this.timeConverter.localUnixEpochDayFromTime( + time, + timezone, + ); + if (unixEpochDay == localBaseDay) return day; + if (unixEpochDay > localBaseDay) return day > 0 ? day - 1 : 6; + return day < 6 ? day + 1 : 0; + }; +} diff --git a/src/modules/ad/interface/dtos/actor.response.dto.ts b/src/modules/ad/interface/dtos/actor.response.dto.ts index bb86ff3..ed701e4 100644 --- a/src/modules/ad/interface/dtos/actor.response.dto.ts +++ b/src/modules/ad/interface/dtos/actor.response.dto.ts @@ -1,10 +1,4 @@ export class ActorResponseDto { role: string; target: string; - firstDatetime: string; - firstMinDatetime: string; - firstMaxDatetime: string; - lastDatetime: string; - lastMinDatetime: string; - lastMaxDatetime: string; } diff --git a/src/modules/ad/interface/dtos/journey.response.dto.ts b/src/modules/ad/interface/dtos/journey.response.dto.ts index f4c5736..2f0aa5c 100644 --- a/src/modules/ad/interface/dtos/journey.response.dto.ts +++ b/src/modules/ad/interface/dtos/journey.response.dto.ts @@ -1,6 +1,7 @@ import { StepResponseDto } from './step.response.dto'; export class JourneyResponseDto { + weekday: number; firstDate: string; lastDate: string; steps: StepResponseDto[]; diff --git a/src/modules/ad/interface/dtos/match.response.dto.ts b/src/modules/ad/interface/dtos/match.response.dto.ts index ce22d4b..8797252 100644 --- a/src/modules/ad/interface/dtos/match.response.dto.ts +++ b/src/modules/ad/interface/dtos/match.response.dto.ts @@ -4,7 +4,14 @@ import { JourneyResponseDto } from './journey.response.dto'; export class MatchResponseDto extends ResponseBase { adId: string; role: string; + frequency: string; distance: number; duration: number; + initialDistance: number; + initialDuration: number; + distanceDetour: number; + durationDetour: number; + distanceDetourPercentage: number; + durationDetourPercentage: number; journeys: JourneyResponseDto[]; } diff --git a/src/modules/ad/interface/dtos/step.response.dto.ts b/src/modules/ad/interface/dtos/step.response.dto.ts index b2240ac..f7083b5 100644 --- a/src/modules/ad/interface/dtos/step.response.dto.ts +++ b/src/modules/ad/interface/dtos/step.response.dto.ts @@ -1,9 +1,10 @@ import { ActorResponseDto } from './actor.response.dto'; export class StepResponseDto { - duration: number; distance: number; + duration: number; lon: number; lat: number; + time: string; actors: ActorResponseDto[]; } diff --git a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index e21c784..cbdf93e 100644 --- a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -1,6 +1,6 @@ import { Controller, Inject, UsePipes } from '@nestjs/common'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { ResponseBase, RpcValidationPipe } from '@mobicoop/ddd-library'; +import { RpcValidationPipe } from '@mobicoop/ddd-library'; import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; import { QueryBus } from '@nestjs/cqrs'; @@ -9,9 +9,7 @@ import { MatchQuery } from '@modules/ad/core/application/queries/match/match.que import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; -import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; -import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; -import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { MatchMapper } from '@modules/ad/match.mapper'; @UsePipes( new RpcValidationPipe({ @@ -25,6 +23,7 @@ export class MatchGrpcController { private readonly queryBus: QueryBus, @Inject(AD_ROUTE_PROVIDER) private readonly routeProvider: RouteProviderPort, + private readonly matchMapper: MatchMapper, ) {} @GrpcMethod('MatcherService', 'Match') @@ -34,33 +33,9 @@ export class MatchGrpcController { new MatchQuery(data, this.routeProvider), ); return new MatchPaginatedResponseDto({ - data: matches.map((match: MatchEntity) => ({ - ...new ResponseBase(match), - adId: match.getProps().adId, - role: match.getProps().role, - distance: match.getProps().distance, - duration: match.getProps().duration, - journeys: match.getProps().journeys.map((journey: Journey) => ({ - firstDate: journey.firstDate.toUTCString(), - lastDate: journey.lastDate.toUTCString(), - steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ - duration: journeyItem.duration, - distance: journeyItem.distance as number, - lon: journeyItem.lon, - lat: journeyItem.lat, - actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ - role: actorTime.role, - target: actorTime.target, - firstDatetime: actorTime.firstMinDatetime.toUTCString(), - firstMinDatetime: actorTime.firstMinDatetime.toUTCString(), - firstMaxDatetime: actorTime.firstMaxDatetime.toUTCString(), - lastDatetime: actorTime.lastDatetime.toUTCString(), - lastMinDatetime: actorTime.lastMinDatetime.toUTCString(), - lastMaxDatetime: actorTime.lastMaxDatetime.toUTCString(), - })), - })), - })), - })), + data: matches.map((match: MatchEntity) => + this.matchMapper.toResponse(match), + ), page: 1, perPage: 5, total: matches.length, diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index 29ac5ff..0bc68f6 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -57,34 +57,36 @@ message Match { string id = 1; string adId = 2; string role = 3; - int32 duration = 4; - int32 distance = 5; - repeated Journey journeys = 6; + int32 distance = 4; + int32 duration = 5; + int32 initialDistance = 6; + int32 initialDuration = 7; + int32 distanceDetour = 8; + int32 durationDetour = 9; + double distanceDetourPercentage = 10; + double durationDetourPercentage = 11; + repeated Journey journeys = 12; } message Journey { - string firstDate = 1; - string lastDate = 2; - repeated Step steps = 3; + int32 weekday = 1; + string firstDate = 2; + string lastDate = 3; + repeated Step steps = 4; } message Step { - int32 duration = 1; - int32 distance = 2; + int32 distance = 1; + int32 duration = 2; double lon = 3; double lat = 4; - repeated Actor actors = 5; + string time = 5; + repeated Actor actors = 6; } message Actor { string role = 1; string target = 2; - string firstDatetime = 3; - string firstMinDatetime = 4; - string firstMaxDatetime = 5; - string lastDatetime = 6; - string lastMinDatetime = 7; - string lastMaxDatetime = 8; } message Matches { diff --git a/src/modules/ad/match.mapper.ts b/src/modules/ad/match.mapper.ts new file mode 100644 index 0000000..91be4ce --- /dev/null +++ b/src/modules/ad/match.mapper.ts @@ -0,0 +1,78 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MatchEntity } from './core/domain/match.entity'; +import { MatchResponseDto } from './interface/dtos/match.response.dto'; +import { ResponseBase } from '@mobicoop/ddd-library'; +import { Journey } from './core/domain/value-objects/journey.value-object'; +import { JourneyItem } from './core/domain/value-objects/journey-item.value-object'; +import { ActorTime } from './core/domain/value-objects/actor-time.value-object'; +import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens'; +import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port'; + +@Injectable() +export class MatchMapper { + constructor( + @Inject(OUTPUT_DATETIME_TRANSFORMER) + private readonly outputDatetimeTransformer: DateTimeTransformerPort, + ) {} + + toResponse = (match: MatchEntity): MatchResponseDto => ({ + ...new ResponseBase(match), + adId: match.getProps().adId, + role: match.getProps().role, + frequency: match.getProps().frequency, + distance: match.getProps().distance, + duration: match.getProps().duration, + initialDistance: match.getProps().initialDistance, + initialDuration: match.getProps().initialDuration, + distanceDetour: match.getProps().distanceDetour, + durationDetour: match.getProps().durationDetour, + distanceDetourPercentage: match.getProps().distanceDetourPercentage, + durationDetourPercentage: match.getProps().durationDetourPercentage, + journeys: match.getProps().journeys.map((journey: Journey) => ({ + weekday: new Date( + this.outputDatetimeTransformer.fromDate( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + ).getDay(), + firstDate: this.outputDatetimeTransformer.fromDate( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + lastDate: this.outputDatetimeTransformer.fromDate( + { + date: journey.lastDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ + duration: journeyItem.duration, + distance: journeyItem.distance as number, + lon: journeyItem.lon, + lat: journeyItem.lat, + time: this.outputDatetimeTransformer.time( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journeyItem.driverTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ + role: actorTime.role, + target: actorTime.target, + })), + })), + })), + }); +} diff --git a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts index 720d5b6..277a2af 100644 --- a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts +++ b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts @@ -67,6 +67,7 @@ class SomeSelector extends Selector { CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts index f1e4b7d..10bb27b 100644 --- a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -1,4 +1,4 @@ -import { Role } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { SpacetimeDetourRatio, @@ -253,6 +253,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -272,6 +273,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -291,6 +293,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -312,6 +315,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -330,6 +334,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -351,6 +356,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -372,6 +378,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', @@ -408,6 +415,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', @@ -454,6 +462,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', @@ -477,6 +486,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', diff --git a/src/modules/ad/tests/unit/core/journey.completer.spec.ts b/src/modules/ad/tests/unit/core/journey.completer.spec.ts index ee9d348..b084679 100644 --- a/src/modules/ad/tests/unit/core/journey.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.completer.spec.ts @@ -65,6 +65,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/journey.filter.spec.ts b/src/modules/ad/tests/unit/core/journey.filter.spec.ts index 8f707c1..239f0f4 100644 --- a/src/modules/ad/tests/unit/core/journey.filter.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.filter.spec.ts @@ -49,6 +49,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/match.entity.spec.ts b/src/modules/ad/tests/unit/core/match.entity.spec.ts index fe57511..b2c0990 100644 --- a/src/modules/ad/tests/unit/core/match.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/match.entity.spec.ts @@ -1,4 +1,4 @@ -import { Role } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; @@ -9,8 +9,11 @@ describe('Match entity create', () => { const match: MatchEntity = MatchEntity.create({ adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', role: Role.DRIVER, + frequency: Frequency.RECURRENT, distance: 356041, duration: 12647, + initialDistance: 315478, + initialDuration: 12105, journeys: [ { firstDate: new Date('2023-09-01'), diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index 7a33fa1..524f982 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -50,6 +50,7 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -98,6 +99,7 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 8bb2344..6c8bf7b 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -49,6 +49,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index 8daaf47..4a39362 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -69,6 +69,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts new file mode 100644 index 0000000..5e9cf9f --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts @@ -0,0 +1,280 @@ +import { + PARAMS_PROVIDER, + TIMEZONE_FINDER, + TIME_CONVERTER, +} from '@modules/ad/ad.di-tokens'; +import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port'; +import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; +import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port'; +import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockDefaultParamsProvider: DefaultParamsProviderPort = { + getParams: () => { + return { + DEPARTURE_TIME_MARGIN: 900, + DRIVER: false, + SEATS_PROPOSED: 3, + PASSENGER: true, + SEATS_REQUESTED: 1, + STRICT: false, + TIMEZONE: 'Europe/Paris', + ALGORITHM_TYPE: AlgorithmType.PASSENGER_ORIENTED, + 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, + }; + }, +}; + +const mockTimezoneFinder: TimezoneFinderPort = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter: TimeConverterPort = { + localStringTimeToUtcStringTime: jest.fn(), + utcStringTimeToLocalStringTime: jest + .fn() + .mockImplementationOnce(() => '00:15'), + localStringDateTimeToUtcDate: jest.fn(), + utcStringDateTimeToLocalIsoString: jest + .fn() + .mockImplementationOnce(() => '2023-07-30T08:15:00.000+02:00') + .mockImplementationOnce(() => '2023-07-20T10:15:00.000+02:00') + .mockImplementationOnce(() => '2023-07-19T23:15:00.000+02:00') + .mockImplementationOnce(() => '2023-07-20T00:15:00.000+02:00'), + utcUnixEpochDayFromTime: jest.fn(), + localUnixEpochDayFromTime: jest + .fn() + .mockImplementationOnce(() => 4) + .mockImplementationOnce(() => 5) + .mockImplementationOnce(() => 5) + .mockImplementationOnce(() => 3) + .mockImplementationOnce(() => 3), +}; + +describe('Output Datetime Transformer', () => { + let outputDatetimeTransformer: OutputDateTimeTransformer; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: PARAMS_PROVIDER, + useValue: mockDefaultParamsProvider, + }, + { + provide: TIMEZONE_FINDER, + useValue: mockTimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useValue: mockTimeConverter, + }, + OutputDateTimeTransformer, + ], + }).compile(); + + outputDatetimeTransformer = module.get( + OutputDateTimeTransformer, + ); + }); + + it('should be defined', () => { + expect(outputDatetimeTransformer).toBeDefined(); + }); + + describe('fromDate', () => { + it('should return fromDate as is if frequency is recurrent', () => { + const transformedFromDate: string = outputDatetimeTransformer.fromDate( + { + date: '2023-07-30', + time: '07:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(transformedFromDate).toBe('2023-07-30'); + }); + it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => { + const transformedFromDate: string = outputDatetimeTransformer.fromDate( + { + date: '2023-07-30', + time: '07:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(transformedFromDate).toBe('2023-07-30'); + }); + }); + + describe('toDate', () => { + it('should return toDate as is if frequency is recurrent', () => { + const transformedToDate: string = outputDatetimeTransformer.toDate( + '2024-07-29', + { + date: '2023-07-20', + time: '10:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(transformedToDate).toBe('2024-07-29'); + }); + it('should return transformed fromDate if frequency is punctual', () => { + const transformedToDate: string = outputDatetimeTransformer.toDate( + '2024-07-30', + { + date: '2023-07-20', + time: '08:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(transformedToDate).toBe('2023-07-20'); + }); + }); + + describe('day', () => { + it('should not change day if frequency is recurrent and converted local time is on the same day', () => { + const day: number = outputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(1); + }); + it('should change day if frequency is recurrent and converted local time is on the next day', () => { + const day: number = outputDatetimeTransformer.day( + 0, + { + date: '2023-07-23', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(1); + }); + it('should change day if frequency is recurrent and converted local time is on the next day and given day is saturday', () => { + const day: number = outputDatetimeTransformer.day( + 6, + { + date: '2023-07-23', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should change day if frequency is recurrent and converted local time is on the previous day', () => { + const day: number = outputDatetimeTransformer.day( + 1, + { + date: '2023-07-25', + time: '00:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should change day if frequency is recurrent and converted local time is on the previous day and given day is sunday(0)', () => { + const day: number = outputDatetimeTransformer.day( + 0, + { + date: '2023-07-30', + time: '00:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(6); + }); + it('should return local fromDate day if frequency is punctual', () => { + const day: number = outputDatetimeTransformer.day( + 1, + { + date: '2023-07-20', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(day).toBe(3); + }); + }); + + describe('time', () => { + it('should transform utc time to local time if frequency is recurrent', () => { + const time: string = outputDatetimeTransformer.time( + { + date: '2023-07-23', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(time).toBe('00:15'); + }); + it('should return local time if frequency is punctual', () => { + const time: string = outputDatetimeTransformer.time( + { + date: '2023-07-19', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(time).toBe('00:15'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 9d39955..44203aa 100644 --- a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -10,6 +10,7 @@ import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item. import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller'; +import { MatchMapper } from '@modules/ad/match.mapper'; import { QueryBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; @@ -33,16 +34,16 @@ const destinationWaypoint: WaypointDto = { country: 'France', }; -const punctualMatchRequestDto: MatchRequestDto = { +const recurrentMatchRequestDto: MatchRequestDto = { driver: false, passenger: true, - frequency: Frequency.PUNCTUAL, + frequency: Frequency.RECURRENT, fromDate: '2023-08-15', - toDate: '2023-08-15', + toDate: '2024-09-30', schedule: [ { time: '07:00', - day: 2, + day: 5, margin: 900, }, ], @@ -58,8 +59,11 @@ const mockQueryBus = { MatchEntity.create({ adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', role: Role.DRIVER, + frequency: Frequency.RECURRENT, distance: 356041, duration: 12647, + initialDistance: 349251, + initialDuration: 12103, journeys: [ { firstDate: new Date('2023-09-01'), @@ -172,6 +176,116 @@ const mockRouteProvider: RouteProviderPort = { getDetailed: jest.fn(), }; +const mockMatchMapper = { + toResponse: jest.fn().mockImplementation(() => ({ + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + role: 'DRIVER', + frequency: 'RECURRENT', + distance: 356041, + duration: 12647, + journeys: [ + { + firstDate: '2023-09-01', + lastDate: '2024-08-30', + journeyItems: [ + { + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + { + role: 'DRIVER', + target: 'START', + firstDatetime: '2023-09-01 07:00', + firstMinDatetime: '2023-09-01 06:45', + firstMaxDatetime: '2023-09-01 07:15', + lastDatetime: '2024-08-30 07:00', + lastMinDatetime: '2024-08-30 06:45', + lastMaxDatetime: '2024-08-30 07:15', + }, + ], + }, + { + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + { + role: 'DRIVER', + target: 'NEUTRAL', + firstDatetime: '2023-09-01 07:35', + firstMinDatetime: '2023-09-01 07:20', + firstMaxDatetime: '2023-09-01 07:50', + lastDatetime: '2024-08-30 07:35', + lastMinDatetime: '2024-08-30 07:20', + lastMaxDatetime: '2024-08-30 07:50', + }, + { + role: 'PASSENGER', + target: 'START', + firstDatetime: '2023-09-01 07:32', + firstMinDatetime: '2023-09-01 07:17', + firstMaxDatetime: '2023-09-01 07:47', + lastDatetime: '2024-08-30 07:32', + lastMinDatetime: '2024-08-30 07:17', + lastMaxDatetime: '2024-08-30 07:47', + }, + ], + }, + { + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + { + role: 'DRIVER', + target: 'NEUTRAL', + firstDatetime: '2023-09-01 08:04', + firstMinDatetime: '2023-09-01 07:51', + firstMaxDatetime: '2023-09-01 08:19', + lastDatetime: '2024-08-30 08:04', + lastMinDatetime: '2024-08-30 07:51', + lastMaxDatetime: '2024-08-30 08:19', + }, + { + role: 'PASSENGER', + target: 'FINISH', + firstDatetime: '2023-09-01 08:01', + firstMinDatetime: '2023-09-01 07:46', + firstMaxDatetime: '2023-09-01 08:16', + lastDatetime: '2024-08-30 08:01', + lastMinDatetime: '2024-08-30 07:46', + lastMaxDatetime: '2024-08-30 08:16', + }, + ], + }, + { + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + { + role: 'DRIVER', + target: 'FINISH', + firstDatetime: '2023-09-01 08:23', + firstMinDatetime: '2023-09-01 08:08', + firstMaxDatetime: '2023-09-01 08:38', + lastDatetime: '2024-08-30 08:23', + lastMinDatetime: '2024-08-30 08:08', + lastMaxDatetime: '2024-08-30 08:38', + }, + ], + }, + ], + }, + ], + })), +}; + describe('Match Grpc Controller', () => { let matchGrpcController: MatchGrpcController; @@ -187,6 +301,10 @@ describe('Match Grpc Controller', () => { provide: AD_ROUTE_PROVIDER, useValue: mockRouteProvider, }, + { + provide: MatchMapper, + useValue: mockMatchMapper, + }, ], }).compile(); @@ -204,7 +322,7 @@ describe('Match Grpc Controller', () => { it('should return matches', async () => { jest.spyOn(mockQueryBus, 'execute'); const matchPaginatedResponseDto = await matchGrpcController.match( - punctualMatchRequestDto, + recurrentMatchRequestDto, ); expect(matchPaginatedResponseDto.data).toHaveLength(1); expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); @@ -214,7 +332,7 @@ describe('Match Grpc Controller', () => { jest.spyOn(mockQueryBus, 'execute'); expect.assertions(3); try { - await matchGrpcController.match(punctualMatchRequestDto); + await matchGrpcController.match(recurrentMatchRequestDto); } catch (e: any) { expect(e).toBeInstanceOf(RpcException); expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); diff --git a/src/modules/ad/tests/unit/match.mapper.spec.ts b/src/modules/ad/tests/unit/match.mapper.spec.ts new file mode 100644 index 0000000..b2ce143 --- /dev/null +++ b/src/modules/ad/tests/unit/match.mapper.spec.ts @@ -0,0 +1,154 @@ +import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens'; +import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; +import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; +import { MatchResponseDto } from '@modules/ad/interface/dtos/match.response.dto'; +import { MatchMapper } from '@modules/ad/match.mapper'; +import { Test } from '@nestjs/testing'; + +const matchEntity: MatchEntity = MatchEntity.create({ + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + role: Role.DRIVER, + frequency: Frequency.RECURRENT, + distance: 356041, + duration: 12647, + initialDistance: 315478, + initialDuration: 12105, + journeys: [ + new Journey({ + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }), + ], +}); + +const mockOutputDatetimeTransformer: DateTimeTransformerPort = { + fromDate: jest.fn(), + toDate: jest.fn(), + day: jest.fn(), + time: jest.fn(), +}; + +describe('Match Mapper', () => { + let matchMapper: MatchMapper; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + providers: [ + MatchMapper, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useValue: mockOutputDatetimeTransformer, + }, + ], + }).compile(); + matchMapper = module.get(MatchMapper); + }); + + it('should be defined', () => { + expect(matchMapper).toBeDefined(); + }); + + it('should map domain entity to response', async () => { + const mapped: MatchResponseDto = matchMapper.toResponse(matchEntity); + expect(mapped.journeys).toHaveLength(1); + }); +}); From 3bc142f4f730eb65975351555abea7fe0d0024ae Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 26 Sep 2023 14:43:54 +0200 Subject: [PATCH 47/52] improve response, fix integration tests --- src/modules/ad/interface/dtos/journey.response.dto.ts | 2 +- src/modules/ad/interface/grpc-controllers/matcher.proto | 2 +- src/modules/ad/match.mapper.ts | 2 +- src/modules/ad/tests/integration/ad.repository.spec.ts | 8 ++------ 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/modules/ad/interface/dtos/journey.response.dto.ts b/src/modules/ad/interface/dtos/journey.response.dto.ts index 2f0aa5c..f01599b 100644 --- a/src/modules/ad/interface/dtos/journey.response.dto.ts +++ b/src/modules/ad/interface/dtos/journey.response.dto.ts @@ -1,7 +1,7 @@ import { StepResponseDto } from './step.response.dto'; export class JourneyResponseDto { - weekday: number; + day: number; firstDate: string; lastDate: string; steps: StepResponseDto[]; diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index 0bc68f6..44dae76 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -69,7 +69,7 @@ message Match { } message Journey { - int32 weekday = 1; + int32 day = 1; string firstDate = 2; string lastDate = 3; repeated Step steps = 4; diff --git a/src/modules/ad/match.mapper.ts b/src/modules/ad/match.mapper.ts index 91be4ce..e8b60c4 100644 --- a/src/modules/ad/match.mapper.ts +++ b/src/modules/ad/match.mapper.ts @@ -29,7 +29,7 @@ export class MatchMapper { distanceDetourPercentage: match.getProps().distanceDetourPercentage, durationDetourPercentage: match.getProps().durationDetourPercentage, journeys: match.getProps().journeys.map((journey: Journey) => ({ - weekday: new Date( + day: new Date( this.outputDatetimeTransformer.fromDate( { date: journey.firstDate.toISOString().split('T')[0], diff --git a/src/modules/ad/tests/integration/ad.repository.spec.ts b/src/modules/ad/tests/integration/ad.repository.spec.ts index c74557c..2b29066 100644 --- a/src/modules/ad/tests/integration/ad.repository.spec.ts +++ b/src/modules/ad/tests/integration/ad.repository.spec.ts @@ -89,12 +89,10 @@ describe('Ad Repository', () => { strict: false, waypoints: [ { - position: 0, lon: 43.7102, lat: 7.262, }, { - position: 1, lon: 43.2965, lat: 5.3698, }, @@ -126,7 +124,7 @@ describe('Ad Repository', () => { }; const adToCreate: AdEntity = AdEntity.create(createAdProps); - await adRepository.insertWithUnsupportedFields(adToCreate, 'ad'); + await adRepository.insertExtra(adToCreate, 'ad'); const afterCount = await prismaService.ad.count(); @@ -175,12 +173,10 @@ describe('Ad Repository', () => { strict: false, waypoints: [ { - position: 0, lon: 43.7102, lat: 7.262, }, { - position: 1, lon: 43.2965, lat: 5.3698, }, @@ -212,7 +208,7 @@ describe('Ad Repository', () => { }; const adToCreate: AdEntity = AdEntity.create(createAdProps); - await adRepository.insertWithUnsupportedFields(adToCreate, 'ad'); + await adRepository.insertExtra(adToCreate, 'ad'); const afterCount = await prismaService.ad.count(); From 5c802df529cf246f11a7d326966fb0aba9e1810a Mon Sep 17 00:00:00 2001 From: sbriat Date: Wed, 27 Sep 2023 08:57:34 +0200 Subject: [PATCH 48/52] handle pagination on query --- .env.dist | 15 ++++++--------- .../application/ports/default-params.type.ts | 1 + .../queries/match/match.query-handler.ts | 4 ++++ .../application/queries/match/match.query.ts | 19 +++++++++++++++---- .../infrastructure/default-params-provider.ts | 5 +++++ .../unit/core/match.query-handler.spec.ts | 1 + .../ad/tests/unit/core/match.query.spec.ts | 1 + .../default-param.provider.spec.ts | 4 ++++ .../input-datetime-transformer.spec.ts | 1 + .../output-datetime-transformer.spec.ts | 1 + 10 files changed, 39 insertions(+), 13 deletions(-) diff --git a/.env.dist b/.env.dist index c4859fc..ceae12f 100644 --- a/.env.dist +++ b/.env.dist @@ -47,19 +47,16 @@ MAX_DETOUR_DURATION_RATIO=0.3 GEOROUTER_TYPE=graphhopper # georouter url GEOROUTER_URL=http://localhost:8989 - -# DEFAULT CARPOOL DEPARTURE TIME MARGIN (in seconds) +# default carpool departure time margin (in seconds) DEPARTURE_TIME_MARGIN=900 - -# DEFAULT ROLE +# default role ROLE=passenger - -# SEATS PROPOSED AS DRIVER / REQUESTED AS PASSENGER +# seats proposes as driver / requested as passenger SEATS_PROPOSED=3 SEATS_REQUESTED=1 - -# ACCEPT ONLY SAME FREQUENCY REQUESTS +# accept only same frequency requests STRICT_FREQUENCY=false - # default timezone TIMEZONE=Europe/Paris +# number of matching results per page +PER_PAGE=10 diff --git a/src/modules/ad/core/application/ports/default-params.type.ts b/src/modules/ad/core/application/ports/default-params.type.ts index 3bee195..9b4c2ca 100644 --- a/src/modules/ad/core/application/ports/default-params.type.ts +++ b/src/modules/ad/core/application/ports/default-params.type.ts @@ -16,4 +16,5 @@ export type DefaultParams = { AZIMUTH_MARGIN: number; MAX_DETOUR_DISTANCE_RATIO: number; MAX_DETOUR_DURATION_RATIO: number; + PER_PAGE: number; }; diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index fb263fc..17300d1 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -49,6 +49,10 @@ export class MatchQueryHandler implements IQueryHandler { maxDetourDistanceRatio: this._defaultParams.MAX_DETOUR_DISTANCE_RATIO, maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO, }) + .setDefaultPagination({ + page: 1, + perPage: this._defaultParams.PER_PAGE, + }) .setDatesAndSchedule(this.datetimeTransformer); await query.setRoutes(); diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index 742a663..eadc26a 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -33,8 +33,8 @@ export class MatchQuery extends QueryBase { azimuthMargin?: number; maxDetourDistanceRatio?: number; maxDetourDurationRatio?: number; - readonly page?: number; - readonly perPage?: number; + page?: number; + perPage?: number; driverRoute?: Route; passengerRoute?: Route; backAzimuth?: number; @@ -61,8 +61,8 @@ export class MatchQuery extends QueryBase { this.azimuthMargin = props.azimuthMargin; this.maxDetourDistanceRatio = props.maxDetourDistanceRatio; this.maxDetourDurationRatio = props.maxDetourDurationRatio; - this.page = props.page ?? 1; - this.perPage = props.perPage ?? 10; + this.page = props.page; + this.perPage = props.perPage; this.originWaypoint = this.waypoints.filter( (waypoint: Waypoint) => waypoint.position == 0, )[0]; @@ -124,6 +124,12 @@ export class MatchQuery extends QueryBase { return this; }; + setDefaultPagination = (defaultPagination: DefaultPagination): MatchQuery => { + if (!this.page) this.page = defaultPagination.page; + if (!this.perPage) this.perPage = defaultPagination.perPage; + return this; + }; + setDatesAndSchedule = ( datetimeTransformer: DateTimeTransformerPort, ): MatchQuery => { @@ -243,3 +249,8 @@ interface DefaultAlgorithmParameters { maxDetourDistanceRatio: number; maxDetourDurationRatio: number; } + +interface DefaultPagination { + page: number; + perPage: number; +} diff --git a/src/modules/ad/infrastructure/default-params-provider.ts b/src/modules/ad/infrastructure/default-params-provider.ts index 81f1737..10d7f13 100644 --- a/src/modules/ad/infrastructure/default-params-provider.ts +++ b/src/modules/ad/infrastructure/default-params-provider.ts @@ -18,6 +18,7 @@ const USE_AZIMUTH = true; const AZIMUTH_MARGIN = 10; const MAX_DETOUR_DISTANCE_RATIO = 0.3; const MAX_DETOUR_DURATION_RATIO = 0.3; +const PER_PAGE = 10; @Injectable() export class DefaultParamsProvider implements DefaultParamsProviderPort { @@ -81,5 +82,9 @@ export class DefaultParamsProvider implements DefaultParamsProviderPort { this._configService.get('MAX_DETOUR_DURATION_RATIO') as string, ) : MAX_DETOUR_DURATION_RATIO, + PER_PAGE: + this._configService.get('PER_PAGE') !== undefined + ? parseInt(this._configService.get('PER_PAGE') as string) + : PER_PAGE, }); } diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 9fd930d..1f308f2 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -72,6 +72,7 @@ const mockDefaultParamsProvider: DefaultParamsProviderPort = { AZIMUTH_MARGIN: 10, MAX_DETOUR_DISTANCE_RATIO: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3, + PER_PAGE: 10, }; }, }; diff --git a/src/modules/ad/tests/unit/core/match.query.spec.ts b/src/modules/ad/tests/unit/core/match.query.spec.ts index 14f7415..62ecb04 100644 --- a/src/modules/ad/tests/unit/core/match.query.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query.spec.ts @@ -49,6 +49,7 @@ const defaultParams: DefaultParams = { AZIMUTH_MARGIN: 10, MAX_DETOUR_DISTANCE_RATIO: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3, + PER_PAGE: 10, }; const mockInputDateTimeTransformer: DateTimeTransformerPort = { diff --git a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts index 105358a..c23ea19 100644 --- a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts @@ -34,6 +34,8 @@ const mockConfigServiceWithDefaults = { return 0.5; case 'MAX_DETOUR_DURATION_RATIO': return 0.6; + case 'PER_PAGE': + return 15; default: return 'some_default_value'; } @@ -96,6 +98,7 @@ describe('DefaultParamsProvider', () => { expect(params.AZIMUTH_MARGIN).toBe(15); expect(params.MAX_DETOUR_DISTANCE_RATIO).toBe(0.5); expect(params.MAX_DETOUR_DURATION_RATIO).toBe(0.6); + expect(params.PER_PAGE).toBe(15); }); it('should provide default params if defaults are not set', async () => { @@ -113,5 +116,6 @@ describe('DefaultParamsProvider', () => { expect(params.AZIMUTH_MARGIN).toBe(10); expect(params.MAX_DETOUR_DISTANCE_RATIO).toBe(0.3); expect(params.MAX_DETOUR_DURATION_RATIO).toBe(0.3); + expect(params.PER_PAGE).toBe(10); }); }); diff --git a/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts index bc4bce3..166b21c 100644 --- a/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts @@ -29,6 +29,7 @@ const mockDefaultParamsProvider: DefaultParamsProviderPort = { AZIMUTH_MARGIN: 10, MAX_DETOUR_DISTANCE_RATIO: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3, + PER_PAGE: 10, }; }, }; diff --git a/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts index 5e9cf9f..efa575f 100644 --- a/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts @@ -29,6 +29,7 @@ const mockDefaultParamsProvider: DefaultParamsProviderPort = { AZIMUTH_MARGIN: 10, MAX_DETOUR_DISTANCE_RATIO: 0.3, MAX_DETOUR_DURATION_RATIO: 0.3, + PER_PAGE: 10, }; }, }; From 09efe313baf3c98d53ba78a72f28cd190143eb24 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 28 Sep 2023 11:03:56 +0200 Subject: [PATCH 49/52] save results to redis --- .env.dist | 4 +- package-lock.json | 8 +- package.json | 2 +- src/modules/ad/ad.di-tokens.ts | 1 + src/modules/ad/ad.module.ts | 9 +- .../ports/matching.repository.port.ts | 6 + .../queries/match/match.query-handler.ts | 82 +++++- src/modules/ad/core/domain/matching.entity.ts | 19 ++ src/modules/ad/core/domain/matching.errors.ts | 11 + src/modules/ad/core/domain/matching.types.ts | 14 + .../value-objects/match-query.value-object.ts | 109 ++++++++ .../ad/infrastructure/matching.repository.ts | 46 ++++ .../ad/infrastructure/time-converter.ts | 2 +- .../dtos/id-paginated.reponse.dto.ts | 7 + .../dtos/match.paginated.response.dto.ts | 6 - .../dtos/matching.paginated.response.dto.ts | 11 + .../grpc-controllers/match.grpc-controller.ts | 18 +- .../interface/grpc-controllers/matcher.proto | 9 +- src/modules/ad/matching.mapper.ts | 13 + .../core/match-query.value-object.spec.ts | 61 +++++ .../unit/core/match.query-handler.spec.ts | 44 +++- .../matching.repository.spec.ts | 228 ++++++++++++++++ .../infrastructure/time-converter.spec.ts | 53 ++-- .../interface/match.grpc.controller.spec.ts | 245 ++++++++++-------- .../ad/tests/unit/matching.mapper.spec.ts | 118 +++++++++ 25 files changed, 950 insertions(+), 176 deletions(-) create mode 100644 src/modules/ad/core/application/ports/matching.repository.port.ts create mode 100644 src/modules/ad/core/domain/matching.entity.ts create mode 100644 src/modules/ad/core/domain/matching.errors.ts create mode 100644 src/modules/ad/core/domain/matching.types.ts create mode 100644 src/modules/ad/core/domain/value-objects/match-query.value-object.ts create mode 100644 src/modules/ad/infrastructure/matching.repository.ts create mode 100644 src/modules/ad/interface/dtos/id-paginated.reponse.dto.ts delete mode 100644 src/modules/ad/interface/dtos/match.paginated.response.dto.ts create mode 100644 src/modules/ad/interface/dtos/matching.paginated.response.dto.ts create mode 100644 src/modules/ad/matching.mapper.ts create mode 100644 src/modules/ad/tests/unit/core/match-query.value-object.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts create mode 100644 src/modules/ad/tests/unit/matching.mapper.spec.ts diff --git a/.env.dist b/.env.dist index ceae12f..5157f6c 100644 --- a/.env.dist +++ b/.env.dist @@ -15,14 +15,14 @@ MESSAGE_BROKER_EXCHANGE=mobicoop REDIS_HOST=v3-redis REDIS_PASSWORD=redis REDIS_PORT=6379 +REDIS_MATCHING_KEY=MATCHER:MATCHING +REDIS_MATCHING_TTL=900 # CACHE CACHE_TTL=5000 # DEFAULT CONFIGURATION -# default identifier used for match requests -DEFAULT_UUID=00000000-0000-0000-0000-000000000000 # algorithm type ALGORITHM=PASSENGER_ORIENTED # max distance in metres between driver diff --git a/package-lock.json b/package-lock.json index ed5a5e3..ea93558 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", "@mobicoop/configuration-module": "^1.2.0", - "@mobicoop/ddd-library": "^1.3.0", + "@mobicoop/ddd-library": "^1.5.0", "@mobicoop/health-module": "^2.0.0", "@mobicoop/message-broker-module": "^1.2.0", "@nestjs/axios": "^2.0.0", @@ -1505,9 +1505,9 @@ } }, "node_modules/@mobicoop/ddd-library": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.3.0.tgz", - "integrity": "sha512-WQTOIzGvsoh3o43Kukb9NIbJw18lsfSqu3k3cMZxc2mmgaYD7MtS4Yif/+KayQ6Ea4Ve3Hc6BVDls2X6svsoOg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.5.0.tgz", + "integrity": "sha512-CX/V2+vSXrGtKobsyBfVpMW323ZT8tHrgUl1qrvU1XjRKNShvwsKyC7739x7CNgkJ9sr3XV+75JrOXEnqU83zw==", "dependencies": { "@nestjs/event-emitter": "^1.4.2", "@nestjs/microservices": "^9.4.0", diff --git a/package.json b/package.json index 8fdf7af..94f9b03 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@grpc/proto-loader": "^0.7.6", "@liaoliaots/nestjs-redis": "^9.0.5", "@mobicoop/configuration-module": "^1.2.0", - "@mobicoop/ddd-library": "^1.3.0", + "@mobicoop/ddd-library": "^1.5.0", "@mobicoop/health-module": "^2.0.0", "@mobicoop/message-broker-module": "^1.2.0", "@nestjs/axios": "^2.0.0", diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index e225a20..ce89895 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -1,4 +1,5 @@ export const AD_REPOSITORY = Symbol('AD_REPOSITORY'); +export const MATCHING_REPOSITORY = Symbol('MATCHING_REPOSITORY'); export const AD_DIRECTION_ENCODER = Symbol('AD_DIRECTION_ENCODER'); export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER'); export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol( diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 90348ac..3756e70 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -12,6 +12,7 @@ import { INPUT_DATETIME_TRANSFORMER, AD_GET_DETAILED_ROUTE_CONTROLLER, OUTPUT_DATETIME_TRANSFORMER, + MATCHING_REPOSITORY, } from './ad.di-tokens'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { AdRepository } from './infrastructure/ad.repository'; @@ -32,6 +33,8 @@ import { InputDateTimeTransformer } from './infrastructure/input-datetime-transf import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller'; import { MatchMapper } from './match.mapper'; import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; +import { MatchingRepository } from './infrastructure/matching.repository'; +import { MatchingMapper } from './matching.mapper'; const grpcControllers = [MatchGrpcController]; @@ -41,13 +44,17 @@ const commandHandlers: Provider[] = [CreateAdService]; const queryHandlers: Provider[] = [MatchQueryHandler]; -const mappers: Provider[] = [AdMapper, MatchMapper]; +const mappers: Provider[] = [AdMapper, MatchMapper, MatchingMapper]; const repositories: Provider[] = [ { provide: AD_REPOSITORY, useClass: AdRepository, }, + { + provide: MATCHING_REPOSITORY, + useClass: MatchingRepository, + }, ]; const messagePublishers: Provider[] = [ diff --git a/src/modules/ad/core/application/ports/matching.repository.port.ts b/src/modules/ad/core/application/ports/matching.repository.port.ts new file mode 100644 index 0000000..f715684 --- /dev/null +++ b/src/modules/ad/core/application/ports/matching.repository.port.ts @@ -0,0 +1,6 @@ +import { MatchingEntity } from '../../domain/matching.entity'; + +export type MatchingRepositoryPort = { + get(id: string): Promise; + save(matching: MatchingEntity): Promise; +}; diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 17300d1..603b438 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -1,5 +1,5 @@ import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; -import { MatchQuery } from './match.query'; +import { MatchQuery, ScheduleItem } from './match.query'; import { Algorithm } from './algorithm.abstract'; import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm'; import { AlgorithmType } from '../../types/algorithm.types'; @@ -8,12 +8,16 @@ import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.reposito import { AD_REPOSITORY, INPUT_DATETIME_TRANSFORMER, + MATCHING_REPOSITORY, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port'; import { DefaultParams } from '../../ports/default-params.type'; import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port'; +import { Paginator } from '@mobicoop/ddd-library'; +import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; +import { MatchingRepositoryPort } from '../../ports/matching.repository.port'; @QueryHandler(MatchQuery) export class MatchQueryHandler implements IQueryHandler { @@ -22,14 +26,16 @@ export class MatchQueryHandler implements IQueryHandler { constructor( @Inject(PARAMS_PROVIDER) private readonly defaultParamsProvider: DefaultParamsProviderPort, - @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, + @Inject(AD_REPOSITORY) private readonly adRepository: AdRepositoryPort, + @Inject(MATCHING_REPOSITORY) + private readonly matchingRepository: MatchingRepositoryPort, @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, ) { this._defaultParams = defaultParamsProvider.getParams(); } - execute = async (query: MatchQuery): Promise => { + execute = async (query: MatchQuery): Promise => { query .setMissingMarginDurations(this._defaultParams.DEPARTURE_TIME_MARGIN) .setMissingStrict(this._defaultParams.STRICT) @@ -60,8 +66,74 @@ export class MatchQueryHandler implements IQueryHandler { switch (query.algorithmType) { case AlgorithmType.PASSENGER_ORIENTED: default: - algorithm = new PassengerOrientedAlgorithm(query, this.repository); + algorithm = new PassengerOrientedAlgorithm(query, this.adRepository); } - return algorithm.match(); + + const matches: MatchEntity[] = await algorithm.match(); + const perPage: number = query.perPage as number; + const page: number = Paginator.pageNumber( + matches.length, + perPage, + query.page as number, + ); + // create Matching Entity for persistence + const matchingEntity: MatchingEntity = MatchingEntity.create({ + matches: matches.map((matchEntity: MatchEntity) => ({ + adId: matchEntity.getProps().adId, + role: matchEntity.getProps().role, + frequency: matchEntity.getProps().frequency, + distance: matchEntity.getProps().distance, + duration: matchEntity.getProps().duration, + initialDistance: matchEntity.getProps().initialDistance, + initialDuration: matchEntity.getProps().initialDuration, + distanceDetour: matchEntity.getProps().distanceDetour, + durationDetour: matchEntity.getProps().durationDetour, + distanceDetourPercentage: + matchEntity.getProps().distanceDetourPercentage, + durationDetourPercentage: + matchEntity.getProps().durationDetourPercentage, + journeys: matchEntity.getProps().journeys, + })), + query: { + driver: query.driver as boolean, + passenger: query.passenger as boolean, + frequency: query.frequency, + fromDate: query.fromDate, + toDate: query.toDate, + schedule: query.schedule.map((scheduleItem: ScheduleItem) => ({ + day: scheduleItem.day as number, + time: scheduleItem.time, + margin: scheduleItem.margin as number, + })), + seatsProposed: query.seatsProposed as number, + seatsRequested: query.seatsRequested as number, + strict: query.strict as boolean, + waypoints: query.waypoints, + algorithmType: query.algorithmType as AlgorithmType, + remoteness: query.remoteness as number, + useProportion: query.useProportion as boolean, + proportion: query.proportion as number, + useAzimuth: query.useAzimuth as boolean, + azimuthMargin: query.azimuthMargin as number, + maxDetourDistanceRatio: query.maxDetourDistanceRatio as number, + maxDetourDurationRatio: query.maxDetourDurationRatio as number, + }, + }); + await this.matchingRepository.save(matchingEntity); + return { + id: matchingEntity.id, + matches: Paginator.pageItems(matches, page, perPage), + total: matches.length, + page, + perPage, + }; }; } + +export type MatchingResult = { + id: string; + matches: MatchEntity[]; + total: number; + page: number; + perPage: number; +}; diff --git a/src/modules/ad/core/domain/matching.entity.ts b/src/modules/ad/core/domain/matching.entity.ts new file mode 100644 index 0000000..619721b --- /dev/null +++ b/src/modules/ad/core/domain/matching.entity.ts @@ -0,0 +1,19 @@ +import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; +import { v4 } from 'uuid'; +import { CreateMatchingProps, MatchingProps } from './matching.types'; + +export class MatchingEntity extends AggregateRoot { + protected readonly _id: AggregateID; + + static create = (create: CreateMatchingProps): MatchingEntity => { + const id = v4(); + const props: MatchingProps = { + ...create, + }; + return new MatchingEntity({ id, props }); + }; + + validate(): void { + // entity business rules validation to protect it's invariant before saving entity to a database + } +} diff --git a/src/modules/ad/core/domain/matching.errors.ts b/src/modules/ad/core/domain/matching.errors.ts new file mode 100644 index 0000000..b1fee32 --- /dev/null +++ b/src/modules/ad/core/domain/matching.errors.ts @@ -0,0 +1,11 @@ +import { ExceptionBase } from '@mobicoop/ddd-library'; + +export class MatchingNotFoundException extends ExceptionBase { + static readonly message = 'Matching error'; + + public readonly code = 'MATCHER.MATCHING_NOT_FOUND'; + + constructor(cause?: Error, metadata?: unknown) { + super(MatchingNotFoundException.message, cause, metadata); + } +} diff --git a/src/modules/ad/core/domain/matching.types.ts b/src/modules/ad/core/domain/matching.types.ts new file mode 100644 index 0000000..ddc67fa --- /dev/null +++ b/src/modules/ad/core/domain/matching.types.ts @@ -0,0 +1,14 @@ +import { MatchProps } from './match.types'; +import { MatchQueryProps } from './value-objects/match-query.value-object'; + +// All properties that a Matching has +export interface MatchingProps { + query: MatchQueryProps; // the query that induced the matches + matches: MatchProps[]; +} + +// Properties that are needed for a Matching creation +export interface CreateMatchingProps { + query: MatchQueryProps; + matches: MatchProps[]; +} diff --git a/src/modules/ad/core/domain/value-objects/match-query.value-object.ts b/src/modules/ad/core/domain/value-objects/match-query.value-object.ts new file mode 100644 index 0000000..3b1e42b --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/match-query.value-object.ts @@ -0,0 +1,109 @@ +import { ValueObject } from '@mobicoop/ddd-library'; +import { Frequency } from '../ad.types'; +import { ScheduleItemProps } from './schedule-item.value-object'; +import { PointProps } from './point.value-object'; + +/** Note: + * Value Objects with multiple properties can contain + * other Value Objects inside if needed. + * */ + +export interface MatchQueryProps { + driver: boolean; + passenger: boolean; + frequency: Frequency; + fromDate: string; + toDate: string; + schedule: ScheduleItemProps[]; + seatsProposed: number; + seatsRequested: number; + strict: boolean; + waypoints: PointProps[]; + algorithmType: string; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDistanceRatio: number; + maxDetourDurationRatio: number; +} + +export class MatchQuery extends ValueObject { + get driver(): boolean { + return this.props.driver; + } + + get passenger(): boolean { + return this.props.passenger; + } + + get frequency(): Frequency { + return this.props.frequency; + } + + get fromDate(): string { + return this.props.fromDate; + } + + get toDate(): string { + return this.props.toDate; + } + + get schedule(): ScheduleItemProps[] { + return this.props.schedule; + } + + get seatsProposed(): number { + return this.props.seatsProposed; + } + + get seatsRequested(): number { + return this.props.seatsRequested; + } + + get strict(): boolean { + return this.props.strict; + } + + get waypoints(): PointProps[] { + return this.props.waypoints; + } + + get algorithmType(): string { + return this.props.algorithmType; + } + + get remoteness(): number { + return this.props.remoteness; + } + + get useProportion(): boolean { + return this.props.useProportion; + } + + get proportion(): number { + return this.props.proportion; + } + + get useAzimuth(): boolean { + return this.props.useAzimuth; + } + + get azimuthMargin(): number { + return this.props.azimuthMargin; + } + + get maxDetourDistanceRatio(): number { + return this.props.maxDetourDistanceRatio; + } + + get maxDetourDurationRatio(): number { + return this.props.maxDetourDurationRatio; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected validate(props: MatchQueryProps): void { + return; + } +} diff --git a/src/modules/ad/infrastructure/matching.repository.ts b/src/modules/ad/infrastructure/matching.repository.ts new file mode 100644 index 0000000..c1c5e1c --- /dev/null +++ b/src/modules/ad/infrastructure/matching.repository.ts @@ -0,0 +1,46 @@ +import { InjectRedis } from '@liaoliaots/nestjs-redis'; +import { MatchingRepositoryPort } from '../core/application/ports/matching.repository.port'; +import { MatchingEntity } from '../core/domain/matching.entity'; +import { Redis } from 'ioredis'; +import { MatchingMapper } from '../matching.mapper'; +import { ConfigService } from '@nestjs/config'; +import { MatchingNotFoundException } from '../core/domain/matching.errors'; + +const REDIS_MATCHING_TTL = 900; +const REDIS_MATCHING_KEY = 'MATCHER:MATCHING'; + +export class MatchingRepository implements MatchingRepositoryPort { + private _redisKey: string; + private _redisTtl: number; + constructor( + @InjectRedis() private readonly redis: Redis, + private readonly configService: ConfigService, + private readonly mapper: MatchingMapper, + ) { + this._redisKey = + this.configService.get('REDIS_MATCHING_KEY') !== undefined + ? (this.configService.get('REDIS_MATCHING_KEY') as string) + : REDIS_MATCHING_KEY; + this._redisTtl = + this.configService.get('REDIS_MATCHING_TTL') !== undefined + ? (this.configService.get('REDIS_MATCHING_TTL') as number) + : REDIS_MATCHING_TTL; + } + + get = async (matchingId: string): Promise => { + const matching: string | null = await this.redis.get( + `${this._redisKey}:${matchingId}`, + ); + if (matching) return this.mapper.toDomain(matching); + throw new MatchingNotFoundException(new Error('Matching not found')); + }; + + save = async (matching: MatchingEntity): Promise => { + await this.redis.set( + `${this._redisKey}:${matching.id}`, + this.mapper.toPersistence(matching), + 'EX', + this._redisTtl, + ); + }; +} diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts index fc3314f..462473c 100644 --- a/src/modules/ad/infrastructure/time-converter.ts +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -33,7 +33,7 @@ export class TimeConverter implements TimeConverterPort { date: string, time: string, timezone: string, - dst?: boolean, + dst = false, ): string => new DateTime(`${date}T${time}`, TimeZone.zone('UTC')) .convert(TimeZone.zone(timezone, dst)) diff --git a/src/modules/ad/interface/dtos/id-paginated.reponse.dto.ts b/src/modules/ad/interface/dtos/id-paginated.reponse.dto.ts new file mode 100644 index 0000000..53c467b --- /dev/null +++ b/src/modules/ad/interface/dtos/id-paginated.reponse.dto.ts @@ -0,0 +1,7 @@ +import { PaginatedResponseDto } from '@mobicoop/ddd-library'; + +export abstract class IdPaginatedResponseDto< + T, +> extends PaginatedResponseDto { + readonly id: string; +} diff --git a/src/modules/ad/interface/dtos/match.paginated.response.dto.ts b/src/modules/ad/interface/dtos/match.paginated.response.dto.ts deleted file mode 100644 index 52f75c8..0000000 --- a/src/modules/ad/interface/dtos/match.paginated.response.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PaginatedResponseDto } from '@mobicoop/ddd-library'; -import { MatchResponseDto } from './match.response.dto'; - -export class MatchPaginatedResponseDto extends PaginatedResponseDto { - readonly data: readonly MatchResponseDto[]; -} diff --git a/src/modules/ad/interface/dtos/matching.paginated.response.dto.ts b/src/modules/ad/interface/dtos/matching.paginated.response.dto.ts new file mode 100644 index 0000000..14d044e --- /dev/null +++ b/src/modules/ad/interface/dtos/matching.paginated.response.dto.ts @@ -0,0 +1,11 @@ +import { MatchResponseDto } from './match.response.dto'; +import { IdPaginatedResponseDto } from './id-paginated.reponse.dto'; + +export class MatchingPaginatedResponseDto extends IdPaginatedResponseDto { + readonly id: string; + readonly data: readonly MatchResponseDto[]; + constructor(props: IdPaginatedResponseDto) { + super(props); + this.id = props.id; + } +} diff --git a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index cbdf93e..930eaa9 100644 --- a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -2,7 +2,7 @@ import { Controller, Inject, UsePipes } from '@nestjs/common'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { RpcValidationPipe } from '@mobicoop/ddd-library'; import { RpcExceptionCode } from '@mobicoop/ddd-library'; -import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; +import { MatchingPaginatedResponseDto } from '../dtos/matching.paginated.response.dto'; import { QueryBus } from '@nestjs/cqrs'; import { MatchRequestDto } from './dtos/match.request.dto'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; @@ -10,6 +10,7 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { MatchMapper } from '@modules/ad/match.mapper'; +import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler'; @UsePipes( new RpcValidationPipe({ @@ -27,18 +28,19 @@ export class MatchGrpcController { ) {} @GrpcMethod('MatcherService', 'Match') - async match(data: MatchRequestDto): Promise { + async match(data: MatchRequestDto): Promise { try { - const matches: MatchEntity[] = await this.queryBus.execute( + const matchingResult: MatchingResult = await this.queryBus.execute( new MatchQuery(data, this.routeProvider), ); - return new MatchPaginatedResponseDto({ - data: matches.map((match: MatchEntity) => + return new MatchingPaginatedResponseDto({ + id: matchingResult.id, + data: matchingResult.matches.map((match: MatchEntity) => this.matchMapper.toResponse(match), ), - page: 1, - perPage: 5, - total: matches.length, + page: matchingResult.page, + perPage: matchingResult.perPage, + total: matchingResult.total, }); } catch (e) { throw new RpcException({ diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index 44dae76..9e2447e 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -24,6 +24,8 @@ message MatchRequest { float maxDetourDistanceRatio = 15; float maxDetourDurationRatio = 16; int32 identifier = 22; + optional int32 page = 23; + optional int32 perPage = 24; } message ScheduleItem { @@ -90,6 +92,9 @@ message Actor { } message Matches { - repeated Match data = 1; - int32 total = 2; + string id = 1; + repeated Match data = 2; + int32 total = 3; + int32 page = 4; + int32 perPage = 5; } diff --git a/src/modules/ad/matching.mapper.ts b/src/modules/ad/matching.mapper.ts new file mode 100644 index 0000000..e470f44 --- /dev/null +++ b/src/modules/ad/matching.mapper.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { Mapper } from '@mobicoop/ddd-library'; +import { MatchingEntity } from './core/domain/matching.entity'; + +@Injectable() +export class MatchingMapper + implements Mapper +{ + toPersistence = (entity: MatchingEntity): string => JSON.stringify(entity); + + toDomain = (record: string): MatchingEntity => + new MatchingEntity(JSON.parse(record)); +} diff --git a/src/modules/ad/tests/unit/core/match-query.value-object.spec.ts b/src/modules/ad/tests/unit/core/match-query.value-object.spec.ts new file mode 100644 index 0000000..c774714 --- /dev/null +++ b/src/modules/ad/tests/unit/core/match-query.value-object.spec.ts @@ -0,0 +1,61 @@ +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchQuery } from '@modules/ad/core/domain/value-objects/match-query.value-object'; + +describe('Match Query value object', () => { + it('should create a match query value object', () => { + const matchQueryVO = new MatchQuery({ + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-09-01', + toDate: '2023-09-01', + schedule: [ + { + day: 5, + time: '07:10', + margin: 900, + }, + ], + seatsProposed: 3, + seatsRequested: 1, + strict: false, + waypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.21548, + lon: 5.65874, + }, + ], + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + }); + expect(matchQueryVO.driver).toBe(false); + expect(matchQueryVO.passenger).toBe(true); + expect(matchQueryVO.frequency).toBe(Frequency.PUNCTUAL); + expect(matchQueryVO.fromDate).toBe('2023-09-01'); + expect(matchQueryVO.toDate).toBe('2023-09-01'); + expect(matchQueryVO.schedule.length).toBe(1); + expect(matchQueryVO.seatsProposed).toBe(3); + expect(matchQueryVO.seatsRequested).toBe(1); + expect(matchQueryVO.strict).toBe(false); + expect(matchQueryVO.waypoints.length).toBe(2); + expect(matchQueryVO.algorithmType).toBe(AlgorithmType.PASSENGER_ORIENTED); + expect(matchQueryVO.remoteness).toBe(15000); + expect(matchQueryVO.useProportion).toBe(true); + expect(matchQueryVO.proportion).toBe(0.3); + expect(matchQueryVO.useAzimuth).toBe(true); + expect(matchQueryVO.azimuthMargin).toBe(10); + expect(matchQueryVO.maxDetourDistanceRatio).toBe(0.3); + expect(matchQueryVO.maxDetourDurationRatio).toBe(0.3); + }); +}); diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 1f308f2..7aeed8f 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,17 +1,22 @@ import { AD_REPOSITORY, INPUT_DATETIME_TRANSFORMER, + MATCHING_REPOSITORY, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; +import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; -import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; +import { + MatchQueryHandler, + MatchingResult, +} from '@modules/ad/core/application/queries/match/match.query-handler'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; -import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; import { Test, TestingModule } from '@nestjs/testing'; const originWaypoint: Waypoint = { @@ -51,9 +56,30 @@ const mockAdRepository = { ], })), }, + { + id: '4431adea-2e10-4032-a743-01d537058914', + getProps: jest.fn().mockImplementation(() => ({ + role: Role.DRIVER, + waypoints: [ + { + lat: 48.698754, + lon: 6.159874, + }, + { + lat: 48.969874, + lon: 2.449875, + }, + ], + })), + }, ]), }; +const mockMatchingRepository: MatchingRepositoryPort = { + get: jest.fn(), + save: jest.fn(), +}; + const mockDefaultParamsProvider: DefaultParamsProviderPort = { getParams: () => { return { @@ -107,6 +133,10 @@ describe('Match Query Handler', () => { provide: AD_REPOSITORY, useValue: mockAdRepository, }, + { + provide: MATCHING_REPOSITORY, + useValue: mockMatchingRepository, + }, { provide: PARAMS_PROVIDER, useValue: mockDefaultParamsProvider, @@ -125,7 +155,8 @@ describe('Match Query Handler', () => { expect(matchQueryHandler).toBeDefined(); }); - it('should return a Match entity', async () => { + it('should return a Matching', async () => { + jest.spyOn(MatchingEntity, 'create'); const matchQuery = new MatchQuery( { algorithmType: AlgorithmType.PASSENGER_ORIENTED, @@ -146,7 +177,10 @@ describe('Match Query Handler', () => { }, mockRouteProvider, ); - const matches: MatchEntity[] = await matchQueryHandler.execute(matchQuery); - expect(matches.length).toBeGreaterThanOrEqual(0); + const matching: MatchingResult = await matchQueryHandler.execute( + matchQuery, + ); + expect(matching.id).toHaveLength(36); + expect(MatchingEntity.create).toHaveBeenCalledTimes(1); }); }); diff --git a/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts new file mode 100644 index 0000000..67ae1ee --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts @@ -0,0 +1,228 @@ +import { getRedisToken } from '@liaoliaots/nestjs-redis'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; +import { MatchingNotFoundException } from '@modules/ad/core/domain/matching.errors'; +import { MatchingRepository } from '@modules/ad/infrastructure/matching.repository'; +import { MatchingMapper } from '@modules/ad/matching.mapper'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockConfigService = { + get: jest.fn().mockImplementation((value: string) => { + switch (value) { + case 'REDIS_MATCHING_KEY': + return 'MATCHER:MATCHING'; + case 'REDIS_MATCHING_TTL': + return 900; + default: + return 'some_default_value'; + } + }), +}; + +const mockEmptyConfigService = { + get: jest.fn().mockImplementation(() => ({})), +}; + +const mockRedis = { + get: jest + .fn() + .mockImplementationOnce(() => null) + .mockImplementation( + () => + '{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-09-27T15:19:36.487Z","_updatedAt":"2023-09-27T15:19:36.487Z","props":{"matches":[{"adId":"2dfed880-28ad-4a2f-83d9-b8b45677387b","role":"DRIVER","frequency":"RECURRENT","distance":509967,"duration":17404,"initialDistance":495197,"initialDuration":16589,"distanceDetour":14770,"durationDetour":815,"distanceDetourPercentage":2.98,"durationDetourPercentage":4.91,"journeys":[{"props":{"firstDate":"2024-01-07T00:00:00.000Z","lastDate":"2024-06-30T00:00:00.000Z","journeyItems":[{"props":{"lon":0.683083,"lat":47.503445,"duration":17,"distance":0,"actorTimes":[{"props":{"role":"DRIVER","target":"START","firstDatetime":"2024-01-07T20:00:00.000Z","firstMinDatetime":"2024-01-07T19:45:00.000Z","firstMaxDatetime":"2024-01-07T20:15:00.000Z","lastDatetime":"2024-06-30T20:00:00.000Z","lastMinDatetime":"2024-06-30T19:45:00.000Z","lastMaxDatetime":"2024-06-30T20:15:00.000Z"}}]}},{"props":{"lon":0.364394,"lat":46.607501,"duration":4199,"distance":112695,"actorTimes":[{"props":{"role":"PASSENGER","target":"START","firstDatetime":"2024-01-07T21:30:00.000Z","firstMinDatetime":"2024-01-07T21:15:00.000Z","firstMaxDatetime":"2024-01-07T21:45:00.000Z","lastDatetime":"2024-06-30T21:30:00.000Z","lastMinDatetime":"2024-06-30T21:15:00.000Z","lastMaxDatetime":"2024-06-30T21:45:00.000Z"}},{"props":{"role":"DRIVER","target":"NEUTRAL","firstDatetime":"2024-01-07T21:09:59.000Z","firstMinDatetime":"2024-01-07T20:54:59.000Z","firstMaxDatetime":"2024-01-07T21:24:59.000Z","lastDatetime":"2024-06-30T21:09:59.000Z","lastMinDatetime":"2024-06-30T20:54:59.000Z","lastMaxDatetime":"2024-06-30T21:24:59.000Z"}}]}},{"props":{"lon":0.559606,"lat":44.175994,"duration":16975,"distance":503502,"actorTimes":[{"props":{"role":"PASSENGER","target":"FINISH","firstDatetime":"2024-01-08T00:42:55.000Z","firstMinDatetime":"2024-01-08T00:27:55.000Z","firstMaxDatetime":"2024-01-08T00:57:55.000Z","lastDatetime":"2024-07-01T00:42:55.000Z","lastMinDatetime":"2024-07-01T00:27:55.000Z","lastMaxDatetime":"2024-07-01T00:57:55.000Z"}},{"props":{"role":"DRIVER","target":"NEUTRAL","firstDatetime":"2024-01-08T00:42:55.000Z","firstMinDatetime":"2024-01-08T00:27:55.000Z","firstMaxDatetime":"2024-01-08T00:57:55.000Z","lastDatetime":"2024-07-01T00:42:55.000Z","lastMinDatetime":"2024-07-01T00:27:55.000Z","lastMaxDatetime":"2024-07-01T00:57:55.000Z"}}]}},{"props":{"lon":0.610873,"lat":44.204195,"duration":17395,"distance":509967,"actorTimes":[{"props":{"role":"DRIVER","target":"FINISH","firstDatetime":"2024-01-08T00:49:55.000Z","firstMinDatetime":"2024-01-08T00:34:55.000Z","firstMaxDatetime":"2024-01-08T01:04:55.000Z","lastDatetime":"2024-07-01T00:49:55.000Z","lastMinDatetime":"2024-07-01T00:34:55.000Z","lastMaxDatetime":"2024-07-01T01:04:55.000Z"}}]}}]}}]},{"adId":"57bc4da9-1ac2-4c63-acc7-5ff1fe6bc380","role":"DRIVER","frequency":"RECURRENT","distance":491989,"duration":18170,"initialDistance":477219,"initialDuration":17355,"distanceDetour":14770,"durationDetour":815,"distanceDetourPercentage":3.1,"durationDetourPercentage":4.7,"journeys":[{"props":{"firstDate":"2024-01-07T00:00:00.000Z","lastDate":"2024-06-30T00:00:00.000Z","journeyItems":[{"props":{"lon":0.683083,"lat":47.503445,"duration":17,"distance":0,"actorTimes":[{"props":{"role":"DRIVER","target":"START","firstDatetime":"2024-01-07T20:10:00.000Z","firstMinDatetime":"2024-01-07T19:55:00.000Z","firstMaxDatetime":"2024-01-07T20:25:00.000Z","lastDatetime":"2024-06-30T20:10:00.000Z","lastMinDatetime":"2024-06-30T19:55:00.000Z","lastMaxDatetime":"2024-06-30T20:25:00.000Z"}}]}},{"props":{"lon":0.364394,"lat":46.607501,"duration":4199,"distance":112695,"actorTimes":[{"props":{"role":"PASSENGER","target":"START","firstDatetime":"2024-01-07T21:30:00.000Z","firstMinDatetime":"2024-01-07T21:15:00.000Z","firstMaxDatetime":"2024-01-07T21:45:00.000Z","lastDatetime":"2024-06-30T21:30:00.000Z","lastMinDatetime":"2024-06-30T21:15:00.000Z","lastMaxDatetime":"2024-06-30T21:45:00.000Z"}},{"props":{"role":"DRIVER","target":"NEUTRAL","firstDatetime":"2024-01-07T21:19:59.000Z","firstMinDatetime":"2024-01-07T21:04:59.000Z","firstMaxDatetime":"2024-01-07T21:34:59.000Z","lastDatetime":"2024-06-30T21:19:59.000Z","lastMinDatetime":"2024-06-30T21:04:59.000Z","lastMaxDatetime":"2024-06-30T21:34:59.000Z"}}]}},{"props":{"lon":0.192701,"lat":46.029224,"duration":7195,"distance":190046,"actorTimes":[{"props":{"role":"DRIVER","target":"INTERMEDIATE","firstDatetime":"2024-01-07T22:09:55.000Z","firstMinDatetime":"2024-01-07T21:54:55.000Z","firstMaxDatetime":"2024-01-07T22:24:55.000Z","lastDatetime":"2024-06-30T22:09:55.000Z","lastMinDatetime":"2024-06-30T21:54:55.000Z","lastMaxDatetime":"2024-06-30T22:24:55.000Z"}}]}},{"props":{"lon":0.559606,"lat":44.175994,"duration":17741,"distance":485523,"actorTimes":[{"props":{"role":"PASSENGER","target":"FINISH","firstDatetime":"2024-01-08T01:05:41.000Z","firstMinDatetime":"2024-01-08T00:50:41.000Z","firstMaxDatetime":"2024-01-08T01:20:41.000Z","lastDatetime":"2024-07-01T01:05:41.000Z","lastMinDatetime":"2024-07-01T00:50:41.000Z","lastMaxDatetime":"2024-07-01T01:20:41.000Z"}},{"props":{"role":"DRIVER","target":"NEUTRAL","firstDatetime":"2024-01-08T01:05:41.000Z","firstMinDatetime":"2024-01-08T00:50:41.000Z","firstMaxDatetime":"2024-01-08T01:20:41.000Z","lastDatetime":"2024-07-01T01:05:41.000Z","lastMinDatetime":"2024-07-01T00:50:41.000Z","lastMaxDatetime":"2024-07-01T01:20:41.000Z"}}]}},{"props":{"lon":0.610873,"lat":44.204195,"duration":18161,"distance":491989,"actorTimes":[{"props":{"role":"DRIVER","target":"FINISH","firstDatetime":"2024-01-08T01:12:41.000Z","firstMinDatetime":"2024-01-08T00:57:41.000Z","firstMaxDatetime":"2024-01-08T01:27:41.000Z","lastDatetime":"2024-07-01T01:12:41.000Z","lastMinDatetime":"2024-07-01T00:57:41.000Z","lastMaxDatetime":"2024-07-01T01:27:41.000Z"}}]}}]}},{"props":{"firstDate":"2024-01-04T00:00:00.000Z","lastDate":"2024-06-27T00:00:00.000Z","journeyItems":[{"props":{"lon":0.683083,"lat":47.503445,"duration":17,"distance":0,"actorTimes":[{"props":{"role":"DRIVER","target":"START","firstDatetime":"2024-01-04T21:00:00.000Z","firstMinDatetime":"2024-01-04T20:45:00.000Z","firstMaxDatetime":"2024-01-04T21:15:00.000Z","lastDatetime":"2024-06-27T21:00:00.000Z","lastMinDatetime":"2024-06-27T20:45:00.000Z","lastMaxDatetime":"2024-06-27T21:15:00.000Z"}}]}},{"props":{"lon":0.364394,"lat":46.607501,"duration":4199,"distance":112695,"actorTimes":[{"props":{"role":"PASSENGER","target":"START","firstDatetime":"2024-01-04T22:20:00.000Z","firstMinDatetime":"2024-01-04T22:05:00.000Z","firstMaxDatetime":"2024-01-04T22:35:00.000Z","lastDatetime":"2024-06-27T22:20:00.000Z","lastMinDatetime":"2024-06-27T22:05:00.000Z","lastMaxDatetime":"2024-06-27T22:35:00.000Z"}},{"props":{"role":"DRIVER","target":"NEUTRAL","firstDatetime":"2024-01-04T22:09:59.000Z","firstMinDatetime":"2024-01-04T21:54:59.000Z","firstMaxDatetime":"2024-01-04T22:24:59.000Z","lastDatetime":"2024-06-27T22:09:59.000Z","lastMinDatetime":"2024-06-27T21:54:59.000Z","lastMaxDatetime":"2024-06-27T22:24:59.000Z"}}]}},{"props":{"lon":0.192701,"lat":46.029224,"duration":7195,"distance":190046,"actorTimes":[{"props":{"role":"DRIVER","target":"INTERMEDIATE","firstDatetime":"2024-01-04T22:59:55.000Z","firstMinDatetime":"2024-01-04T22:44:55.000Z","firstMaxDatetime":"2024-01-04T23:14:55.000Z","lastDatetime":"2024-06-27T22:59:55.000Z","lastMinDatetime":"2024-06-27T22:44:55.000Z","lastMaxDatetime":"2024-06-27T23:14:55.000Z"}}]}},{"props":{"lon":0.559606,"lat":44.175994,"duration":17741,"distance":485523,"actorTimes":[{"props":{"role":"PASSENGER","target":"FINISH","firstDatetime":"2024-01-05T01:55:41.000Z","firstMinDatetime":"2024-01-05T01:40:41.000Z","firstMaxDatetime":"2024-01-05T02:10:41.000Z","lastDatetime":"2024-06-28T01:55:41.000Z","lastMinDatetime":"2024-06-28T01:40:41.000Z","lastMaxDatetime":"2024-06-28T02:10:41.000Z"}},{"props":{"role":"DRIVER","target":"NEUTRAL","firstDatetime":"2024-01-05T01:55:41.000Z","firstMinDatetime":"2024-01-05T01:40:41.000Z","firstMaxDatetime":"2024-01-05T02:10:41.000Z","lastDatetime":"2024-06-28T01:55:41.000Z","lastMinDatetime":"2024-06-28T01:40:41.000Z","lastMaxDatetime":"2024-06-28T02:10:41.000Z"}}]}},{"props":{"lon":0.610873,"lat":44.204195,"duration":18161,"distance":491989,"actorTimes":[{"props":{"role":"DRIVER","target":"FINISH","firstDatetime":"2024-01-05T02:02:41.000Z","firstMinDatetime":"2024-01-05T01:47:41.000Z","firstMaxDatetime":"2024-01-05T02:17:41.000Z","lastDatetime":"2024-06-28T02:02:41.000Z","lastMinDatetime":"2024-06-28T01:47:41.000Z","lastMaxDatetime":"2024-06-28T02:17:41.000Z"}}]}}]}}]}],"query":{"driver":false,"passenger":true,"frequency":"RECURRENT","fromDate":"2024-01-02","toDate":"2024-06-30","schedule":[{"day":0,"time":"21:30","margin":900},{"day":4,"time":"22:20","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"position":0,"lon":0.364394,"lat":46.607501,"houseNumber":"298","street":"Aveue de la liberté","locality":"Buxerolles","postalCode":"86180","country":"France"},{"position":1,"lon":0.559606,"lat":44.175994,"houseNumber":"1","street":"place du 8 mai 1945","locality":"Roquefort","postalCode":"47310","country":"France"}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}', + ), + set: jest.fn(), +}; + +const mockMapper = { + toDomain: jest.fn().mockImplementation(() => ({ + id: '644a7cb3-6436-4db5-850d-b4c7421d4b97', + createdAt: '2023-09-27T15:19:36.487Z', + updatedAt: '2023-09-27T15:19:36.487Z', + props: [], + })), + toPersistence: jest.fn(), +}; + +const matchingEntity: MatchingEntity = new MatchingEntity({ + id: '644a7cb3-6436-4db5-850d-b4c7421d4b97', + createdAt: new Date(), + updatedAt: new Date(), + props: { + matches: [ + { + adId: 'dd937edf-1264-4868-b073-d1952abe30b1', + role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, + distance: 356041, + duration: 12647, + initialDistance: 348745, + initialDuration: 12105, + distanceDetour: 7296, + durationDetour: 542, + distanceDetourPercentage: 4.1, + durationDetourPercentage: 3.8, + journeys: [ + { + firstDate: new Date('2023-09-01'), + lastDate: new Date('2023-09-01'), + journeyItems: [ + { + lon: 6.35484, + lat: 48.26587, + duration: 0, + distance: 0, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + ], + }, + ], + }, + ], + // ... + }, + ], + query: { + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-09-01', + toDate: '2023-09-01', + schedule: [ + { + day: 5, + time: '06:40', + margin: 900, + }, + ], + seatsProposed: 3, + seatsRequested: 1, + strict: true, + waypoints: [ + { + lon: 6.389745, + lat: 48.32644, + }, + { + lon: 6.984567, + lat: 48.021548, + }, + ], + algorithmType: 'PASSENGER_ORIENTED', + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + }, + }, +}); + +describe('Matching repository', () => { + let matchingRepository: MatchingRepository; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MatchingRepository, + { + provide: getRedisToken('default'), + useValue: mockRedis, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + { + provide: MatchingMapper, + useValue: mockMapper, + }, + ], + }).compile(); + + matchingRepository = module.get(MatchingRepository); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(matchingRepository).toBeDefined(); + }); + it('should throw an exception if a matching is not found', async () => { + await expect( + matchingRepository.get('644a7cb3-6436-4db5-850d-b4c7421d4b98'), + ).rejects.toBeInstanceOf(MatchingNotFoundException); + }); + it('should get a matching', async () => { + const matching: MatchingEntity = await matchingRepository.get( + '644a7cb3-6436-4db5-850d-b4c7421d4b97', + ); + expect(matching.id).toBe('644a7cb3-6436-4db5-850d-b4c7421d4b97'); + }); + it('should save a matching', async () => { + jest.spyOn(mockRedis, 'set'); + await matchingRepository.save(matchingEntity); + expect(mockRedis.set).toHaveBeenCalledTimes(1); + }); +}); + +describe('Matching repository without env vars', () => { + let secondMatchingRepository: MatchingRepository; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MatchingRepository, + { + provide: getRedisToken('default'), + useValue: mockRedis, + }, + { + provide: ConfigService, + useValue: mockEmptyConfigService, + }, + { + provide: MatchingMapper, + useValue: mockMapper, + }, + ], + }).compile(); + + secondMatchingRepository = + module.get(MatchingRepository); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(secondMatchingRepository).toBeDefined(); + }); + it('should get a matching', async () => { + const matching: MatchingEntity = await secondMatchingRepository.get( + '644a7cb3-6436-4db5-850d-b4c7421d4b97', + ); + expect(matching.id).toBe('644a7cb3-6436-4db5-850d-b4c7421d4b97'); + }); + it('should save a matching', async () => { + jest.spyOn(mockRedis, 'set'); + await secondMatchingRepository.save(matchingEntity); + expect(mockRedis.set).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts index ce8ca75..45941f1 100644 --- a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts @@ -52,7 +52,7 @@ describe('Time Converter', () => { }); describe('localStringDateTimeToUtcDate', () => { - it('should convert a summer paris date and time to a utc date', () => { + it('should convert a summer paris date and time to a utc date with dst', () => { const timeConverter: TimeConverter = new TimeConverter(); const parisDate = '2023-06-22'; const parisTime = '12:00'; @@ -64,7 +64,7 @@ describe('Time Converter', () => { ); expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z'); }); - it('should convert a winter paris date and time to a utc date', () => { + it('should convert a winter paris date and time to a utc date with dst', () => { const timeConverter: TimeConverter = new TimeConverter(); const parisDate = '2023-02-02'; const parisTime = '12:00'; @@ -72,6 +72,7 @@ describe('Time Converter', () => { parisDate, parisTime, 'Europe/Paris', + true, ); expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z'); }); @@ -83,7 +84,6 @@ describe('Time Converter', () => { parisDate, parisTime, 'Europe/Paris', - false, ); expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z'); }); @@ -148,6 +148,30 @@ describe('Time Converter', () => { }); describe('utcStringDateTimeToLocalIsoString', () => { + it('should convert a utc string date and time to a summer paris date isostring with dst', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + true, + ); + expect(localIsoString).toBe('2023-06-22T12:00:00.000+02:00'); + }); + it('should convert a utc string date and time to a winter paris date isostring with dst', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-02'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + true, + ); + expect(localIsoString).toBe('2023-02-02T11:00:00.000+01:00'); + }); it('should convert a utc string date and time to a summer paris date isostring', () => { const timeConverter: TimeConverter = new TimeConverter(); const utcDate = '2023-06-22'; @@ -157,29 +181,6 @@ describe('Time Converter', () => { utcTime, 'Europe/Paris', ); - expect(localIsoString).toBe('2023-06-22T12:00:00.000+02:00'); - }); - it('should convert a utc string date and time to a winter paris date isostring', () => { - const timeConverter: TimeConverter = new TimeConverter(); - const utcDate = '2023-02-02'; - const utcTime = '10:00'; - const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( - utcDate, - utcTime, - 'Europe/Paris', - ); - expect(localIsoString).toBe('2023-02-02T11:00:00.000+01:00'); - }); - it('should convert a utc string date and time to a summer paris date isostring without dst', () => { - const timeConverter: TimeConverter = new TimeConverter(); - const utcDate = '2023-06-22'; - const utcTime = '10:00'; - const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( - utcDate, - utcTime, - 'Europe/Paris', - false, - ); expect(localIsoString).toBe('2023-06-22T11:00:00.000+01:00'); }); it('should convert a utc date to a tonga date isostring', () => { diff --git a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 44203aa..a138f04 100644 --- a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -1,12 +1,14 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; +import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; +import { MatchingPaginatedResponseDto } from '@modules/ad/interface/dtos/matching.paginated.response.dto'; import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller'; @@ -55,117 +57,126 @@ const recurrentMatchRequestDto: MatchRequestDto = { const mockQueryBus = { execute: jest .fn() - .mockImplementationOnce(() => [ - MatchEntity.create({ - adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', - role: Role.DRIVER, - frequency: Frequency.RECURRENT, - distance: 356041, - duration: 12647, - initialDistance: 349251, - initialDuration: 12103, - journeys: [ - { - firstDate: new Date('2023-09-01'), - lastDate: new Date('2024-08-30'), - journeyItems: [ - new JourneyItem({ - lat: 48.689445, - lon: 6.17651, - duration: 0, - distance: 0, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.START, - firstDatetime: new Date('2023-09-01 07:00'), - firstMinDatetime: new Date('2023-09-01 06:45'), - firstMaxDatetime: new Date('2023-09-01 07:15'), - lastDatetime: new Date('2024-08-30 07:00'), - lastMinDatetime: new Date('2024-08-30 06:45'), - lastMaxDatetime: new Date('2024-08-30 07:15'), - }), - ], - }), - new JourneyItem({ - lat: 48.369445, - lon: 6.67487, - duration: 2100, - distance: 56878, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.NEUTRAL, - firstDatetime: new Date('2023-09-01 07:35'), - firstMinDatetime: new Date('2023-09-01 07:20'), - firstMaxDatetime: new Date('2023-09-01 07:50'), - lastDatetime: new Date('2024-08-30 07:35'), - lastMinDatetime: new Date('2024-08-30 07:20'), - lastMaxDatetime: new Date('2024-08-30 07:50'), - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.START, - firstDatetime: new Date('2023-09-01 07:32'), - firstMinDatetime: new Date('2023-09-01 07:17'), - firstMaxDatetime: new Date('2023-09-01 07:47'), - lastDatetime: new Date('2024-08-30 07:32'), - lastMinDatetime: new Date('2024-08-30 07:17'), - lastMaxDatetime: new Date('2024-08-30 07:47'), - }), - ], - }), - new JourneyItem({ - lat: 47.98487, - lon: 6.9427, - duration: 3840, - distance: 76491, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.NEUTRAL, - firstDatetime: new Date('2023-09-01 08:04'), - firstMinDatetime: new Date('2023-09-01 07:51'), - firstMaxDatetime: new Date('2023-09-01 08:19'), - lastDatetime: new Date('2024-08-30 08:04'), - lastMinDatetime: new Date('2024-08-30 07:51'), - lastMaxDatetime: new Date('2024-08-30 08:19'), - }), - new ActorTime({ - role: Role.PASSENGER, - target: Target.FINISH, - firstDatetime: new Date('2023-09-01 08:01'), - firstMinDatetime: new Date('2023-09-01 07:46'), - firstMaxDatetime: new Date('2023-09-01 08:16'), - lastDatetime: new Date('2024-08-30 08:01'), - lastMinDatetime: new Date('2024-08-30 07:46'), - lastMaxDatetime: new Date('2024-08-30 08:16'), - }), - ], - }), - new JourneyItem({ - lat: 47.365987, - lon: 7.02154, - duration: 4980, - distance: 96475, - actorTimes: [ - new ActorTime({ - role: Role.DRIVER, - target: Target.FINISH, - firstDatetime: new Date('2023-09-01 08:23'), - firstMinDatetime: new Date('2023-09-01 08:08'), - firstMaxDatetime: new Date('2023-09-01 08:38'), - lastDatetime: new Date('2024-08-30 08:23'), - lastMinDatetime: new Date('2024-08-30 08:08'), - lastMaxDatetime: new Date('2024-08-30 08:38'), - }), - ], - }), - ], - }, - ], - }), - ]) + .mockImplementationOnce( + () => + { + id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4', + page: 1, + perPage: 10, + matches: [ + MatchEntity.create({ + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + role: Role.DRIVER, + frequency: Frequency.RECURRENT, + distance: 356041, + duration: 12647, + initialDistance: 349251, + initialDuration: 12103, + journeys: [ + { + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }, + ], + }), + ], + total: 1, + }, + ) .mockImplementationOnce(() => { throw new Error(); }), @@ -319,12 +330,16 @@ describe('Match Grpc Controller', () => { expect(matchGrpcController).toBeDefined(); }); - it('should return matches', async () => { + it('should return a matching', async () => { jest.spyOn(mockQueryBus, 'execute'); - const matchPaginatedResponseDto = await matchGrpcController.match( - recurrentMatchRequestDto, + const matchingPaginatedResponseDto: MatchingPaginatedResponseDto = + await matchGrpcController.match(recurrentMatchRequestDto); + expect(matchingPaginatedResponseDto.id).toBe( + '43c83ae2-f4b0-4ac6-b8bf-8071801924d4', ); - expect(matchPaginatedResponseDto.data).toHaveLength(1); + expect(matchingPaginatedResponseDto.data).toHaveLength(1); + expect(matchingPaginatedResponseDto.page).toBe(1); + expect(matchingPaginatedResponseDto.perPage).toBe(10); expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); }); diff --git a/src/modules/ad/tests/unit/matching.mapper.spec.ts b/src/modules/ad/tests/unit/matching.mapper.spec.ts new file mode 100644 index 0000000..fdc4f2e --- /dev/null +++ b/src/modules/ad/tests/unit/matching.mapper.spec.ts @@ -0,0 +1,118 @@ +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; +import { MatchingMapper } from '@modules/ad/matching.mapper'; +import { Test } from '@nestjs/testing'; + +describe('Matching Mapper', () => { + let matchingMapper: MatchingMapper; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + providers: [MatchingMapper], + }).compile(); + matchingMapper = module.get(MatchingMapper); + }); + + it('should be defined', () => { + expect(matchingMapper).toBeDefined(); + }); + + it('should map domain entity to persistence', async () => { + const matchingEntity: MatchingEntity = new MatchingEntity({ + id: '644a7cb3-6436-4db5-850d-b4c7421d4b97', + createdAt: new Date('2023-08-20T09:48:00Z'), + updatedAt: new Date('2023-08-20T09:48:00Z'), + props: { + matches: [ + { + adId: 'dd937edf-1264-4868-b073-d1952abe30b1', + role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, + distance: 356041, + duration: 12647, + initialDistance: 348745, + initialDuration: 12105, + distanceDetour: 7296, + durationDetour: 542, + distanceDetourPercentage: 4.1, + durationDetourPercentage: 3.8, + journeys: [ + { + firstDate: new Date('2023-09-01'), + lastDate: new Date('2023-09-01'), + journeyItems: [ + { + lon: 6.35484, + lat: 48.26587, + duration: 0, + distance: 0, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + ], + }, + ], + }, + ], + // ... + }, + ], + query: { + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-09-01', + toDate: '2023-09-01', + schedule: [ + { + day: 5, + time: '06:40', + margin: 900, + }, + ], + seatsProposed: 3, + seatsRequested: 1, + strict: true, + waypoints: [ + { + lon: 6.389745, + lat: 48.32644, + }, + { + lon: 6.984567, + lat: 48.021548, + }, + ], + algorithmType: 'PASSENGER_ORIENTED', + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + }, + }, + }); + const mapped: string = matchingMapper.toPersistence(matchingEntity); + expect(mapped).toBe( + '{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}', + ); + }); + + it('should map persisted string to domain entity', async () => { + const matchingEntity: MatchingEntity = matchingMapper.toDomain( + '{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}', + ); + expect(matchingEntity.getProps().query.fromDate).toBe('2023-09-01'); + }); +}); From b810bc86e631a6178b3bd4f41363bffdb14e8b41 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 28 Sep 2023 15:23:17 +0200 Subject: [PATCH 50/52] get cached matching --- .../queries/match/match.query-handler.ts | 72 +++--- .../application/queries/match/match.query.ts | 2 + src/modules/ad/core/domain/matching.types.ts | 6 +- .../dtos/match.request.dto.ts | 5 + .../interface/grpc-controllers/matcher.proto | 39 +-- src/modules/ad/match.mapper.ts | 94 ++++---- src/modules/ad/matching.mapper.ts | 227 +++++++++++++++++- .../unit/core/match.query-handler.spec.ts | 217 ++++++++++++++++- .../matching.repository.spec.ts | 9 +- .../ad/tests/unit/matching.mapper.spec.ts | 130 ++++++---- 10 files changed, 650 insertions(+), 151 deletions(-) diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 603b438..141bbad 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -60,6 +60,44 @@ export class MatchQueryHandler implements IQueryHandler { perPage: this._defaultParams.PER_PAGE, }) .setDatesAndSchedule(this.datetimeTransformer); + let matchingEntity: MatchingEntity | undefined = await this._cachedMatching( + query.id, + ); + if (!matchingEntity) + matchingEntity = (await this._createMatching(query)) as MatchingEntity; + const perPage: number = query.perPage as number; + const page: number = Paginator.pageNumber( + matchingEntity.getProps().matches.length, + perPage, + query.page as number, + ); + return { + id: matchingEntity.id, + matches: Paginator.pageItems( + matchingEntity.getProps().matches, + page, + perPage, + ), + total: matchingEntity.getProps().matches.length, + page, + perPage, + }; + }; + + private _cachedMatching = async ( + id?: string, + ): Promise => { + if (!id) return undefined; + try { + return await this.matchingRepository.get(id); + } catch (e: any) { + return undefined; + } + }; + + private _createMatching = async ( + query: MatchQuery, + ): Promise => { await query.setRoutes(); let algorithm: Algorithm; @@ -70,30 +108,8 @@ export class MatchQueryHandler implements IQueryHandler { } const matches: MatchEntity[] = await algorithm.match(); - const perPage: number = query.perPage as number; - const page: number = Paginator.pageNumber( - matches.length, - perPage, - query.page as number, - ); - // create Matching Entity for persistence - const matchingEntity: MatchingEntity = MatchingEntity.create({ - matches: matches.map((matchEntity: MatchEntity) => ({ - adId: matchEntity.getProps().adId, - role: matchEntity.getProps().role, - frequency: matchEntity.getProps().frequency, - distance: matchEntity.getProps().distance, - duration: matchEntity.getProps().duration, - initialDistance: matchEntity.getProps().initialDistance, - initialDuration: matchEntity.getProps().initialDuration, - distanceDetour: matchEntity.getProps().distanceDetour, - durationDetour: matchEntity.getProps().durationDetour, - distanceDetourPercentage: - matchEntity.getProps().distanceDetourPercentage, - durationDetourPercentage: - matchEntity.getProps().durationDetourPercentage, - journeys: matchEntity.getProps().journeys, - })), + const matchingEntity = MatchingEntity.create({ + matches, query: { driver: query.driver as boolean, passenger: query.passenger as boolean, @@ -120,13 +136,7 @@ export class MatchQueryHandler implements IQueryHandler { }, }); await this.matchingRepository.save(matchingEntity); - return { - id: matchingEntity.id, - matches: Paginator.pageItems(matches, page, perPage), - total: matches.length, - page, - perPage, - }; + return matchingEntity; }; } diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index eadc26a..937ed4c 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -15,6 +15,7 @@ import { Point } from '@modules/ad/core/domain/value-objects/point.value-object' import { Route } from '../../types/route.type'; export class MatchQuery extends QueryBase { + id?: string; driver?: boolean; passenger?: boolean; readonly frequency: Frequency; @@ -43,6 +44,7 @@ export class MatchQuery extends QueryBase { constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) { super(); + this.id = props.id; this.driver = props.driver; this.passenger = props.passenger; this.frequency = props.frequency; diff --git a/src/modules/ad/core/domain/matching.types.ts b/src/modules/ad/core/domain/matching.types.ts index ddc67fa..fa41021 100644 --- a/src/modules/ad/core/domain/matching.types.ts +++ b/src/modules/ad/core/domain/matching.types.ts @@ -1,14 +1,14 @@ -import { MatchProps } from './match.types'; +import { MatchEntity } from './match.entity'; import { MatchQueryProps } from './value-objects/match-query.value-object'; // All properties that a Matching has export interface MatchingProps { query: MatchQueryProps; // the query that induced the matches - matches: MatchProps[]; + matches: MatchEntity[]; } // Properties that are needed for a Matching creation export interface CreateMatchingProps { query: MatchQueryProps; - matches: MatchProps[]; + matches: MatchEntity[]; } diff --git a/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts index c073e44..70131e2 100644 --- a/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts @@ -7,6 +7,7 @@ import { IsISO8601, IsInt, IsOptional, + IsUUID, Max, Min, ValidateNested, @@ -21,6 +22,10 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; export class MatchRequestDto { + @IsUUID() + @IsOptional() + id?: string; + @IsOptional() @IsBoolean() driver?: boolean; diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index 9e2447e..2144731 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -7,25 +7,26 @@ service MatcherService { } message MatchRequest { - bool driver = 1; - bool passenger = 2; - Frequency frequency = 3; - string fromDate = 4; - string toDate = 5; - repeated ScheduleItem schedule = 6; - bool strict = 7; - repeated Waypoint waypoints = 8; - AlgorithmType algorithmType = 9; - int32 remoteness = 10; - bool useProportion = 11; - int32 proportion = 12; - bool useAzimuth = 13; - int32 azimuthMargin = 14; - float maxDetourDistanceRatio = 15; - float maxDetourDurationRatio = 16; - int32 identifier = 22; - optional int32 page = 23; - optional int32 perPage = 24; + string id = 1; + bool driver = 2; + bool passenger = 3; + Frequency frequency = 4; + string fromDate = 5; + string toDate = 6; + repeated ScheduleItem schedule = 7; + bool strict = 8; + repeated Waypoint waypoints = 9; + AlgorithmType algorithmType = 10; + int32 remoteness = 11; + bool useProportion = 12; + int32 proportion = 13; + bool useAzimuth = 14; + int32 azimuthMargin = 15; + float maxDetourDistanceRatio = 16; + float maxDetourDurationRatio = 17; + int32 identifier = 18; + optional int32 page = 19; + optional int32 perPage = 20; } message ScheduleItem { diff --git a/src/modules/ad/match.mapper.ts b/src/modules/ad/match.mapper.ts index e8b60c4..f0e47fb 100644 --- a/src/modules/ad/match.mapper.ts +++ b/src/modules/ad/match.mapper.ts @@ -15,22 +15,32 @@ export class MatchMapper { private readonly outputDatetimeTransformer: DateTimeTransformerPort, ) {} - toResponse = (match: MatchEntity): MatchResponseDto => ({ - ...new ResponseBase(match), - adId: match.getProps().adId, - role: match.getProps().role, - frequency: match.getProps().frequency, - distance: match.getProps().distance, - duration: match.getProps().duration, - initialDistance: match.getProps().initialDistance, - initialDuration: match.getProps().initialDuration, - distanceDetour: match.getProps().distanceDetour, - durationDetour: match.getProps().durationDetour, - distanceDetourPercentage: match.getProps().distanceDetourPercentage, - durationDetourPercentage: match.getProps().durationDetourPercentage, - journeys: match.getProps().journeys.map((journey: Journey) => ({ - day: new Date( - this.outputDatetimeTransformer.fromDate( + toResponse = (match: MatchEntity): MatchResponseDto => { + return { + ...new ResponseBase(match), + adId: match.getProps().adId, + role: match.getProps().role, + frequency: match.getProps().frequency, + distance: match.getProps().distance, + duration: match.getProps().duration, + initialDistance: match.getProps().initialDistance, + initialDuration: match.getProps().initialDuration, + distanceDetour: match.getProps().distanceDetour, + durationDetour: match.getProps().durationDetour, + distanceDetourPercentage: match.getProps().distanceDetourPercentage, + durationDetourPercentage: match.getProps().durationDetourPercentage, + journeys: match.getProps().journeys.map((journey: Journey) => ({ + day: new Date( + this.outputDatetimeTransformer.fromDate( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + ).getDay(), + firstDate: this.outputDatetimeTransformer.fromDate( { date: journey.firstDate.toISOString().split('T')[0], time: journey.firstDriverDepartureTime(), @@ -38,41 +48,33 @@ export class MatchMapper { }, match.getProps().frequency, ), - ).getDay(), - firstDate: this.outputDatetimeTransformer.fromDate( - { - date: journey.firstDate.toISOString().split('T')[0], - time: journey.firstDriverDepartureTime(), - coordinates: journey.driverOrigin(), - }, - match.getProps().frequency, - ), - lastDate: this.outputDatetimeTransformer.fromDate( - { - date: journey.lastDate.toISOString().split('T')[0], - time: journey.firstDriverDepartureTime(), - coordinates: journey.driverOrigin(), - }, - match.getProps().frequency, - ), - steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ - duration: journeyItem.duration, - distance: journeyItem.distance as number, - lon: journeyItem.lon, - lat: journeyItem.lat, - time: this.outputDatetimeTransformer.time( + lastDate: this.outputDatetimeTransformer.fromDate( { - date: journey.firstDate.toISOString().split('T')[0], - time: journeyItem.driverTime(), + date: journey.lastDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), coordinates: journey.driverOrigin(), }, match.getProps().frequency, ), - actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ - role: actorTime.role, - target: actorTime.target, + steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ + duration: journeyItem.duration, + distance: journeyItem.distance as number, + lon: journeyItem.lon, + lat: journeyItem.lat, + time: this.outputDatetimeTransformer.time( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journeyItem.driverTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ + role: actorTime.role, + target: actorTime.target, + })), })), })), - })), - }); + }; + }; } diff --git a/src/modules/ad/matching.mapper.ts b/src/modules/ad/matching.mapper.ts index e470f44..700d260 100644 --- a/src/modules/ad/matching.mapper.ts +++ b/src/modules/ad/matching.mapper.ts @@ -1,13 +1,234 @@ import { Injectable } from '@nestjs/common'; import { Mapper } from '@mobicoop/ddd-library'; import { MatchingEntity } from './core/domain/matching.entity'; +import { Frequency, Role } from './core/domain/ad.types'; +import { MatchEntity } from './core/domain/match.entity'; +import { Target } from './core/domain/candidate.types'; +import { Waypoint } from './core/application/types/waypoint.type'; +import { ScheduleItem } from './core/application/types/schedule-item.type'; +import { Journey } from './core/domain/value-objects/journey.value-object'; +import { JourneyItem } from './core/domain/value-objects/journey-item.value-object'; +import { ActorTime } from './core/domain/value-objects/actor-time.value-object'; @Injectable() export class MatchingMapper implements Mapper { - toPersistence = (entity: MatchingEntity): string => JSON.stringify(entity); + toPersistence = (entity: MatchingEntity): string => + JSON.stringify({ + id: entity.id, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), + matches: entity.getProps().matches.map((match: MatchEntity) => ({ + adId: match.getProps().adId, + role: match.getProps().role, + frequency: match.getProps().frequency, + distance: match.getProps().distance, + duration: match.getProps().duration, + initialDistance: match.getProps().initialDistance, + initialDuration: match.getProps().initialDuration, + distanceDetour: match.getProps().distanceDetour, + durationDetour: match.getProps().durationDetour, + distanceDetourPercentage: match.getProps().distanceDetourPercentage, + durationDetourPercentage: match.getProps().durationDetourPercentage, + journeys: match.getProps().journeys.map((journey: Journey) => ({ + firstDate: journey.firstDate.toISOString(), + lastDate: journey.lastDate.toISOString(), + journeyItems: journey.journeyItems.map( + (journeyItem: JourneyItem) => ({ + lon: journeyItem.lon, + lat: journeyItem.lat, + duration: journeyItem.duration, + distance: journeyItem.distance, + actorTimes: journeyItem.actorTimes.map( + (actorTime: ActorTime) => ({ + role: actorTime.role, + target: actorTime.target, + firstDatetime: actorTime.firstDatetime.toISOString(), + firstMinDatetime: actorTime.firstMinDatetime.toISOString(), + firstMaxDatetime: actorTime.firstMaxDatetime.toISOString(), + lastDatetime: actorTime.lastDatetime.toISOString(), + lastMinDatetime: actorTime.lastMinDatetime.toISOString(), + lastMaxDatetime: actorTime.lastMaxDatetime.toISOString(), + }), + ), + }), + ), + })), + })), + query: { + driver: entity.getProps().query.driver, + passenger: entity.getProps().query.passenger, + frequency: entity.getProps().query.frequency, + fromDate: entity.getProps().query.fromDate, + toDate: entity.getProps().query.toDate, + schedule: entity + .getProps() + .query.schedule.map((scheduleItem: ScheduleItem) => ({ + day: scheduleItem.day, + time: scheduleItem.time, + margin: scheduleItem.margin, + })), + seatsProposed: entity.getProps().query.seatsProposed, + seatsRequested: entity.getProps().query.seatsRequested, + strict: entity.getProps().query.strict, + waypoints: entity + .getProps() + .query.waypoints.map((waypoint: Waypoint) => ({ + lon: waypoint.lon, + lat: waypoint.lat, + position: waypoint.position, + houseNumber: waypoint.houseNumber, + street: waypoint.street, + postalCode: waypoint.postalCode, + locality: waypoint.locality, + country: waypoint.country, + })), + algorithmType: entity.getProps().query.algorithmType, + remoteness: entity.getProps().query.remoteness, + useProportion: entity.getProps().query.useProportion, + proportion: entity.getProps().query.proportion, + useAzimuth: entity.getProps().query.useAzimuth, + azimuthMargin: entity.getProps().query.azimuthMargin, + maxDetourDistanceRatio: entity.getProps().query.maxDetourDistanceRatio, + maxDetourDurationRatio: entity.getProps().query.maxDetourDurationRatio, + }, + }); - toDomain = (record: string): MatchingEntity => - new MatchingEntity(JSON.parse(record)); + toDomain = (record: string): MatchingEntity => { + const parsedRecord: PersistedMatching = JSON.parse(record); + const matchingEntity: MatchingEntity = new MatchingEntity({ + id: parsedRecord.id, + createdAt: new Date(parsedRecord.createdAt), + updatedAt: new Date(parsedRecord.updatedAt), + props: { + query: parsedRecord.query, + matches: parsedRecord.matches.map((match: PersistedMatch) => + MatchEntity.create({ + adId: match.adId, + role: match.role, + frequency: match.frequency, + distance: match.distance, + duration: match.duration, + initialDistance: match.initialDistance, + initialDuration: match.initialDuration, + journeys: match.journeys.map( + (journey: PersistedJourney) => + new Journey({ + firstDate: new Date(journey.firstDate), + lastDate: new Date(journey.lastDate), + journeyItems: journey.journeyItems.map( + (journeyItem: PersistedJourneyItem) => + new JourneyItem({ + lon: journeyItem.lon, + lat: journeyItem.lat, + duration: journeyItem.duration, + distance: journeyItem.distance, + actorTimes: journeyItem.actorTimes.map( + (actorTime: PersistedActorTime) => + new ActorTime({ + role: actorTime.role, + target: actorTime.target, + firstDatetime: new Date(actorTime.firstDatetime), + firstMinDatetime: new Date( + actorTime.firstMinDatetime, + ), + firstMaxDatetime: new Date( + actorTime.firstMaxDatetime, + ), + lastDatetime: new Date(actorTime.lastDatetime), + lastMinDatetime: new Date( + actorTime.lastMinDatetime, + ), + lastMaxDatetime: new Date( + actorTime.lastMaxDatetime, + ), + }), + ), + }), + ), + }), + ), + }), + ), + }, + }); + return matchingEntity; + }; } + +type PersistedMatching = { + id: string; + createdAt: string; + updatedAt: string; + matches: PersistedMatch[]; + query: { + driver: boolean; + passenger: boolean; + frequency: Frequency; + fromDate: string; + toDate: string; + schedule: { + day: number; + time: string; + margin: number; + }[]; + seatsProposed: number; + seatsRequested: number; + strict: boolean; + waypoints: { + houseNumber: string; + street: string; + postalCode: string; + locality: string; + lon: number; + lat: number; + country: string; + position: number; + }[]; + algorithmType: string; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDistanceRatio: number; + maxDetourDurationRatio: number; + }; +}; + +type PersistedMatch = { + adId: string; + role: Role; + frequency: Frequency; + distance: number; + duration: number; + initialDistance: number; + initialDuration: number; + journeys: PersistedJourney[]; +}; + +type PersistedJourney = { + firstDate: string; + lastDate: string; + journeyItems: PersistedJourneyItem[]; +}; + +type PersistedJourneyItem = { + lon: number; + lat: number; + duration: number; + distance: number; + actorTimes: PersistedActorTime[]; +}; + +type PersistedActorTime = { + role: Role; + target: Target; + firstDatetime: string; + firstMinDatetime: string; + firstMaxDatetime: string; + lastDatetime: string; + lastMinDatetime: string; + lastMaxDatetime: string; +}; diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index 7aeed8f..bc60516 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -16,6 +16,8 @@ import { import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; import { Test, TestingModule } from '@nestjs/testing'; @@ -76,7 +78,156 @@ const mockAdRepository = { }; const mockMatchingRepository: MatchingRepositoryPort = { - get: jest.fn(), + get: jest + .fn() + .mockImplementationOnce( + () => + new MatchingEntity({ + id: 'a3b10efb-121e-4d08-9198-9f57afdb5e2d', + createdAt: new Date('2023-08-20T09:48:00Z'), + updatedAt: new Date('2023-08-20T09:48:00Z'), + props: { + matches: [ + new MatchEntity({ + id: '4bd4e90b-ffba-4f5f-b904-48ad0667a1d7', + createdAt: new Date('2023-08-30T08:45:00Z'), + updatedAt: new Date('2023-08-30T08:45:00Z'), + props: { + adId: 'dd937edf-1264-4868-b073-d1952abe30b1', + role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, + distance: 356041, + duration: 12647, + initialDistance: 348745, + initialDuration: 12105, + distanceDetour: 7296, + durationDetour: 542, + distanceDetourPercentage: 4.1, + durationDetourPercentage: 3.8, + journeys: [ + { + firstDate: new Date('2023-08-28'), + lastDate: new Date('2023-08-28'), + journeyItems: [ + { + lon: 6.389745, + lat: 48.32644, + duration: 0, + distance: 0, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + ], + }, + { + lon: 6.984567, + lat: 48.021548, + distance: 356041, + duration: 12647, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + ], + }, + ], + }, + ], + }, + }), + ], + query: { + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + day: 1, + time: '06:40', + margin: 900, + }, + ], + seatsProposed: 3, + seatsRequested: 1, + strict: true, + waypoints: [ + { + lon: 6.389745, + lat: 48.32644, + }, + { + lon: 6.984567, + lat: 48.021548, + }, + ], + algorithmType: 'PASSENGER_ORIENTED', + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + }, + }, + }), + ) + .mockImplementationOnce(() => { + throw new Error(); + }), save: jest.fn(), }; @@ -151,6 +302,10 @@ describe('Match Query Handler', () => { matchQueryHandler = module.get(MatchQueryHandler); }); + afterEach(async () => { + jest.clearAllMocks(); + }); + it('should be defined', () => { expect(matchQueryHandler).toBeDefined(); }); @@ -183,4 +338,64 @@ describe('Match Query Handler', () => { expect(matching.id).toHaveLength(36); expect(MatchingEntity.create).toHaveBeenCalledTimes(1); }); + + it('should return a valid saved Matching', async () => { + jest.spyOn(MatchingEntity, 'create'); + const matchQuery = new MatchQuery( + { + id: 'a3b10efb-121e-4d08-9198-9f57afdb5e2d', + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + day: 1, + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + const matching: MatchingResult = await matchQueryHandler.execute( + matchQuery, + ); + expect(matching.id).toBe('a3b10efb-121e-4d08-9198-9f57afdb5e2d'); + expect(MatchingEntity.create).toHaveBeenCalledTimes(0); + }); + + it('should return a new matching if saved Matching is not found', async () => { + jest.spyOn(MatchingEntity, 'create'); + const matchQuery = new MatchQuery( + { + id: 'a3b10efb-121e-4d08-9198-9f57afdb5e2d', + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + day: 1, + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + const matching: MatchingResult = await matchQueryHandler.execute( + matchQuery, + ); + expect(matching.id).toHaveLength(36); + expect(MatchingEntity.create).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts index 67ae1ee..bf4795a 100644 --- a/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts @@ -1,6 +1,7 @@ import { getRedisToken } from '@liaoliaots/nestjs-redis'; import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; import { MatchingNotFoundException } from '@modules/ad/core/domain/matching.errors'; import { MatchingRepository } from '@modules/ad/infrastructure/matching.repository'; @@ -52,7 +53,7 @@ const matchingEntity: MatchingEntity = new MatchingEntity({ updatedAt: new Date(), props: { matches: [ - { + MatchEntity.create({ adId: 'dd937edf-1264-4868-b073-d1952abe30b1', role: Role.DRIVER, frequency: Frequency.PUNCTUAL, @@ -60,10 +61,6 @@ const matchingEntity: MatchingEntity = new MatchingEntity({ duration: 12647, initialDistance: 348745, initialDuration: 12105, - distanceDetour: 7296, - durationDetour: 542, - distanceDetourPercentage: 4.1, - durationDetourPercentage: 3.8, journeys: [ { firstDate: new Date('2023-09-01'), @@ -91,7 +88,7 @@ const matchingEntity: MatchingEntity = new MatchingEntity({ }, ], // ... - }, + }), ], query: { driver: false, diff --git a/src/modules/ad/tests/unit/matching.mapper.spec.ts b/src/modules/ad/tests/unit/matching.mapper.spec.ts index fdc4f2e..2b79b0d 100644 --- a/src/modules/ad/tests/unit/matching.mapper.spec.ts +++ b/src/modules/ad/tests/unit/matching.mapper.spec.ts @@ -1,5 +1,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; import { MatchingMapper } from '@modules/ad/matching.mapper'; import { Test } from '@nestjs/testing'; @@ -25,46 +26,88 @@ describe('Matching Mapper', () => { updatedAt: new Date('2023-08-20T09:48:00Z'), props: { matches: [ - { - adId: 'dd937edf-1264-4868-b073-d1952abe30b1', - role: Role.DRIVER, - frequency: Frequency.PUNCTUAL, - distance: 356041, - duration: 12647, - initialDistance: 348745, - initialDuration: 12105, - distanceDetour: 7296, - durationDetour: 542, - distanceDetourPercentage: 4.1, - durationDetourPercentage: 3.8, - journeys: [ - { - firstDate: new Date('2023-09-01'), - lastDate: new Date('2023-09-01'), - journeyItems: [ - { - lon: 6.35484, - lat: 48.26587, - duration: 0, - distance: 0, - actorTimes: [ - { - role: Role.DRIVER, - target: Target.START, - firstDatetime: new Date('2023-09-01T07:00:00Z'), - firstMinDatetime: new Date('2023-09-01T06:45:00Z'), - firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), - lastDatetime: new Date('2023-09-01T07:00:00Z'), - lastMinDatetime: new Date('2023-09-01T06:45:00Z'), - lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), - }, - ], - }, - ], - }, - ], - // ... - }, + new MatchEntity({ + id: '4bd4e90b-ffba-4f5f-b904-48ad0667a1d7', + createdAt: new Date('2023-08-30T08:45:00Z'), + updatedAt: new Date('2023-08-30T08:45:00Z'), + props: { + adId: 'dd937edf-1264-4868-b073-d1952abe30b1', + role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, + distance: 356041, + duration: 12647, + initialDistance: 348745, + initialDuration: 12105, + distanceDetour: 7296, + durationDetour: 542, + distanceDetourPercentage: 4.1, + durationDetourPercentage: 3.8, + journeys: [ + { + firstDate: new Date('2023-09-01'), + lastDate: new Date('2023-09-01'), + journeyItems: [ + { + lon: 6.389745, + lat: 48.32644, + duration: 0, + distance: 0, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + ], + }, + { + lon: 6.984567, + lat: 48.021548, + distance: 356041, + duration: 12647, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + ], + }, + ], + }, + ], + }, + }), ], query: { driver: false, @@ -105,14 +148,17 @@ describe('Matching Mapper', () => { }); const mapped: string = matchingMapper.toPersistence(matchingEntity); expect(mapped).toBe( - '{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}', + '{"id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","createdAt":"2023-08-20T09:48:00.000Z","updatedAt":"2023-08-20T09:48:00.000Z","matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.389745,"lat":48.32644,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]},{"lon":6.984567,"lat":48.021548,"duration":12647,"distance":356041,"actorTimes":[{"role":"DRIVER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}}', ); }); it('should map persisted string to domain entity', async () => { const matchingEntity: MatchingEntity = matchingMapper.toDomain( - '{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}', + '{"id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","createdAt":"2023-08-20T09:48:00.000Z","updatedAt":"2023-08-20T09:48:00.000Z","matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.389745,"lat":48.32644,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]},{"lon":6.984567,"lat":48.021548,"duration":12647,"distance":356041,"actorTimes":[{"role":"DRIVER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}}', ); expect(matchingEntity.getProps().query.fromDate).toBe('2023-09-01'); + expect(matchingEntity.getProps().matches[0].getProps().adId).toBe( + 'dd937edf-1264-4868-b073-d1952abe30b1', + ); }); }); From 2b59e9912161bb244dd8cccd098da6c65215a3e2 Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 28 Sep 2023 16:31:26 +0200 Subject: [PATCH 51/52] udpate readme --- README.md | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/README.md b/README.md index 31944d4..7d655f8 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,232 @@ You need [Docker](https://docs.docker.com/engine/) and its [compose](https://doc You also need NodeJS installed locally : we **strongly** advise to install [Node Version Manager](https://github.com/nvm-sh/nvm) and use the latest LTS version of Node (check that your local version matches with the one used in the Dockerfile). The API will run inside a docker container, **but** the install itself is made outside the container, because during development we need tools that need to be available locally (eg. ESLint or Prettier with fix-on-save). + +A RabbitMQ instance is also required to send / receive messages when data has been inserted/updated/deleted. + +# Installation + +- copy `.env.dist` to `.env` : + + ```bash + cp .env.dist .env + ``` + + Modify it if needed. + +- install the dependencies : + + ```bash + npm install + ``` + +- start the containers : + + ```bash + docker compose up -d + ``` + + The app runs automatically on port **5005**. + +## Database migration + +Before using the app, you need to launch the database migration (it will be launched inside the container) : + +```bash +npm run migrate +``` + +## Usage + +The app exposes the following [gRPC](https://grpc.io/) services : + +- **Match** : find matching ads corresponding to the given criteria + + For example, as a passenger, to search for drivers for a punctual carpool : + + ```json + { + "driver": false, + "passenger": true, + "frequency": "PUNCTUAL", + "algorithmType": "PASSENGER_ORIENTED", + "fromDate": "2024-06-05", + "toDate": "2024-06-05", + "schedule": [ + { + "time": "07:30" + } + ], + "waypoints": [ + { + "houseNumber": "23", + "street": "rue de viller", + "postalCode": "54300", + "locality": "Lunéville", + "lon": 6.490527, + "lat": 48.590119, + "country": "France", + "position": 0 + }, + { + "houseNumber": "3", + "street": "rue du passage", + "postalCode": "67117", + "locality": "Ittenheim", + "lon": 7.594361, + "lat": 48.603004, + "country": "France", + "position": 1 + } + ] + } + ``` + + As a passenger, to search for drivers for a recurrent carpool : + + ```json + { + "driver": false, + "passenger": true, + "frequency": "RECURRENT", + "algorithmType": "PASSENGER_ORIENTED", + "fromDate": "2024-01-02", + "toDate": "2024-06-30", + "strict": true, + "page": 1, + "perPage": 5, + "schedule": [ + { + "day": 1, + "time": "07:30" + }, + { + "day": 2, + "time": "07:45" + }, + { + "day": 4, + "time": "07:30" + }, + , + { + "day": 5, + "time": "07:30" + } + ], + "waypoints": [ + { + "houseNumber": "298", + "street": "Aveue de la liberté", + "postalCode": "86180", + "locality": "Buxerolles", + "lon": 0.364394, + "lat": 46.607501, + "country": "France", + "position": 0 + }, + { + "houseNumber": "1", + "street": "place du 8 mai 1945", + "postalCode": "47310", + "locality": "Roquefort", + "lon": 0.559606, + "lat": 44.175994, + "country": "France", + "position": 1 + } + ] + } + ``` + + The list of possible criteria : + + - **id** (optional): the id of a previous matching result (as a uuid) + - **driver** (boolean, optional): to search for passengers (_default : false_) + - **passenger** (boolean, optional): to search fo drivers (_default : true_) + - **frequency**: the frequency of the search (`PUNCTUAL` or `RECURRENT`) + - **strict** (boolean, optional): if set to true, allow matching only with similar frequency ads (_default : false_) + - **fromDate**: start date for recurrent ad, carpool date for punctual ad + - **toDate**: end date for recurrent ad, same as fromDate for punctual ad + - **schedule**: an array of schedule items, a schedule item containing : + - the week day as a number, from 0 (sunday) to 6 (saturday) if the ad is recurrent (default to fromDate day for punctual search) + - the departure time (as HH:MM) + - the margin around the departure time in seconds (optional) (_default : 900_) + - **seatsProposed** (integer, optional): number of seats proposed as driver (_default : 3_) + - **seatsRequested** (integer, optional): number of seats requested as passenger (_default : 1_) + - **waypoints**: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives + - **algorithmType** (optional): the type of algorithm to use (as of 2023-09-28, only the default `PASSENGER_ORIENTED` is accepted) + - **remoteness** (integer, optional): an integer to indicate the maximum flying distance (in metres) between the driver route and the passenger pick-up / drop-off points (_default : 15000_) + - **useProportion** (boolean, optional): a boolean to indicate if the matching algorithm will compare the distance of the passenger route against the distance of the driver route (_default : 1_). Works in combination with **proportion** parameter + - **proportion** (float, optional): a fraction (float between 0 and 1) to indicate minimum proportion of the distance of the passenger route against the distance of the driver route (_default : 0.3_). Works in combination with **use_proportion** parameter + - **useAzimuth** (boolean, optional): a boolean to indicate if the matching algorithm will use the azimuth of the driver and passenger routes (_default : 1_) + - **azimuthMargin** (integer, optional): an integer (representing the number of degrees) to indicate the range around the opposite azimuth to consider the candidate route excluded (_default : 10_) + - **maxDetourDistanceRatio** (float, optional): a fraction (float between 0 and 1) of the driver route distance to indicate the maximum detour distance acceptable for a passenger (_default : 0.3_) + - **maxDetourDurationRatio** (float, optional): a fraction (float between 0 and 1) of the driver route duration to indicate the maximum detour duration acceptable for a passenger (_default : 0.3_) + - **page** (integer, optional): the page of results to display (_default : 1_) + - **perPage** (integer, optional): the number of results to display per page (_default : 10_) + +If the matching is successful, you will get a result, containing : + +- **id**: the id of the matching; as matching is a time-consuming process, results are cached and thus accessible later using this id (pagination works as well !) +- **total**: the total number of results +- **page**: the number of the page that is returned (may be different than the number of the required page, if number of results does not match with perPage parameter) +- **perPage**: the number of results per page (as it may not be specified in the request) +- **data**: an array of the results themselves, each including: + - **id**: an id for the result + - **adId**: the id of the ad that matches + - **role**: the role of the ad owner in that match + - **distance**: the distance in metres of the resulting carpool + - **duration**: the duration in seconds of the resulting carpool + - **initialDistance**: the initial distance in metres for the driver + - **initialDuration**: the initial duration in seconds for the driver + - **distanceDetour**: the detour distance in metres + - **durationDetour**: the detour duration in seconds + - **distanceDetourPercentage**: the detour distance in percentage of the original distance + - **durationDetourPercentage**: the detour duration in percentage of the original duration + - **journeys**: the possible journeys for the carpool (one journey for punctual carpools, one or more journeys for recurrent carpools), each including: + - **day**: the week day for the journey, as a number, from 0 (sunday) to 6 (saturday) + - **firstDate**: the first possible date for the journey + - **lastDate**: the last possible date for the journey + - **steps**: the steps of the journey (coordinates with distance, duration and actors implied), each including: + - **distance**: the distance to reach the step in metres + - **duration**: the duration to reach the step in seconds + - **lon**: the longitude of the point for the step + - **lat**: the longitude of the point for the step + - **time**: the driver time at that step + - **actors**: the actors for that step: + - **role**: the role of the actor (`DRIVER` or `PASSENGER`) + - **target**: the meaning of the step for the actor: + - _START_ for the first point of the actor + - _FINISH_ for the last point of the actor + - _INTERMEDIATE_ for a driver intermediate point + - _NEUTRAL_ for a passenger point from the point of view of a driver + +## Tests / ESLint / Prettier + +Tests are run outside the container for ease of use (switching between different environments inside containers using prisma is complicated and error prone). +The integration tests use a dedicated database (see _db-test_ section of _docker-compose.yml_). + +```bash +# run all tests (unit + integration) +npm run test + +# unit tests only +npm run test:unit + +# integration tests only +npm run test:integration + +# coverage +npm run test:cov + +# ESLint +npm run lint + +# Prettier +npm run pretty +``` + +## License + +Mobicoop V3 - Matcher Service is [AGPL licensed](LICENSE). From b8d0d8e63191eabbb0d8e6a1bcae0d8c0b80a1cf Mon Sep 17 00:00:00 2001 From: sbriat Date: Thu, 28 Sep 2023 16:32:28 +0200 Subject: [PATCH 52/52] 1.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea93558..ce905e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mobicoop/matcher", - "version": "0.0.2", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mobicoop/matcher", - "version": "0.0.2", + "version": "1.0.0", "license": "AGPL", "dependencies": { "@grpc/grpc-js": "^1.8.14", diff --git a/package.json b/package.json index 94f9b03..32d120f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mobicoop/matcher", - "version": "0.0.2", + "version": "1.0.0", "description": "Mobicoop V3 Matcher", "author": "sbriat", "private": true,