diff --git a/.env.dist b/.env.dist index c793c22..412486b 100644 --- a/.env.dist +++ b/.env.dist @@ -5,10 +5,39 @@ SERVICE_CONFIGURATION_DOMAIN=MATCHER HEALTH_SERVICE_PORT=6005 # DEFAULT CONFIGURATION + +# default identifier used for match requests DEFAULT_IDENTIFIER=0 -MARGIN_DURATION=900 -VALIDITY_DURATION=365 +# default timezone DEFAULT_TIMEZONE=Europe/Paris +# 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 +# max distance in metres between driver +# route and passenger pick-up / drop-off +REMOTENESS=15000 +# use passenger proportion +USE_PROPORTION=1 +# minimal driver proportion +PROPORTION=0.3 +# use azimuth calculation +USE_AZIMUTH=1 +# azimuth margin +AZIMUTH_MARGIN=10 +# margin duration in seconds +MARGIN_DURATION=900 +# default validity duration (in days) for recurrent proposals +VALIDITY_DURATION=365 +# max detour ratio +MAX_DETOUR_DISTANCE_RATIO=0.3 +MAX_DETOUR_DURATION_RATIO=0.3 + # PRISMA DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index 95d5077..be1ddeb 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -14,6 +14,24 @@ export class DefaultParamsProvider { MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')), VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')), DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'), + DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')), + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: this.configService.get('ALGORITHM'), + strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')), + remoteness: parseInt(this.configService.get('REMOTENESS')), + useProportion: !!parseInt(this.configService.get('USE_PROPORTION')), + proportion: parseInt(this.configService.get('PROPORTION')), + useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')), + azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')), + maxDetourDistanceRatio: parseFloat( + this.configService.get('MAX_DETOUR_DISTANCE_RATIO'), + ), + maxDetourDurationRatio: parseFloat( + this.configService.get('MAX_DETOUR_DURATION_RATIO'), + ), + georouterType: this.configService.get('GEOROUTER_TYPE'), + georouterUrl: this.configService.get('GEOROUTER_URL'), + }, }; } } diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 0a96193..d3aa3cd 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -17,9 +17,16 @@ import { Algorithm } from './algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface'; import { IRequestGeography } from '../interfaces/geography-request.interface'; +import { IRequestRequirement } from '../interfaces/requirement-request.interface'; +import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface'; export class MatchRequest - implements IRequestTime, IRequestPerson, IRequestGeography + implements + IRequestTime, + IRequestPerson, + IRequestGeography, + IRequestRequirement, + IRequestAlgorithmSettings { @IsArray() @AutoMap() @@ -65,11 +72,15 @@ export class MatchRequest @IsOptional() @IsNumber() + @Min(1) + @Max(10) @AutoMap() seatsPassenger: number; @IsOptional() @IsNumber() + @Min(1) + @Max(10) @AutoMap() seatsDriver: number; diff --git a/src/modules/matcher/domain/entities/algorithm-settings.ts b/src/modules/matcher/domain/entities/algorithm-settings.ts new file mode 100644 index 0000000..278dea3 --- /dev/null +++ b/src/modules/matcher/domain/entities/algorithm-settings.ts @@ -0,0 +1,58 @@ +import { Algorithm } from '../dtos/algorithm.enum'; +import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface'; +import { DefaultAlgorithmSettings } from '../interfaces/default-algorithm-settings.type'; +import { TimingFrequency } from './timing'; + +export class AlgorithmSettings { + _algorithmSettingsRequest: IRequestAlgorithmSettings; + _strict: boolean; + algorithm: Algorithm; + restrict: TimingFrequency; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDurationRatio: number; + maxDetourDistanceRatio: number; + georouterType: string; + georouterUrl: string; + + constructor( + algorithmSettingsRequest: IRequestAlgorithmSettings, + defaultAlgorithmSettings: DefaultAlgorithmSettings, + frequency: TimingFrequency, + ) { + this._algorithmSettingsRequest = algorithmSettingsRequest; + this.algorithm = + algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm; + this._strict = + algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict; + this.remoteness = algorithmSettingsRequest.remoteness + ? Math.abs(algorithmSettingsRequest.remoteness) + : defaultAlgorithmSettings.remoteness; + this.useProportion = + algorithmSettingsRequest.useProportion ?? + defaultAlgorithmSettings.useProportion; + this.proportion = algorithmSettingsRequest.proportion + ? Math.abs(algorithmSettingsRequest.proportion) + : defaultAlgorithmSettings.proportion; + this.useAzimuth = + algorithmSettingsRequest.useAzimuth ?? + defaultAlgorithmSettings.useAzimuth; + this.azimuthMargin = algorithmSettingsRequest.azimuthMargin + ? Math.abs(algorithmSettingsRequest.azimuthMargin) + : defaultAlgorithmSettings.azimuthMargin; + this.maxDetourDistanceRatio = + algorithmSettingsRequest.maxDetourDistanceRatio ?? + defaultAlgorithmSettings.maxDetourDistanceRatio; + this.maxDetourDurationRatio = + algorithmSettingsRequest.maxDetourDurationRatio ?? + defaultAlgorithmSettings.maxDetourDurationRatio; + this.georouterType = defaultAlgorithmSettings.georouterType; + this.georouterUrl = defaultAlgorithmSettings.georouterUrl; + if (this._strict) { + this.restrict = frequency; + } + } +} diff --git a/src/modules/matcher/domain/entities/requirement.ts b/src/modules/matcher/domain/entities/requirement.ts index b9c9b7c..194907f 100644 --- a/src/modules/matcher/domain/entities/requirement.ts +++ b/src/modules/matcher/domain/entities/requirement.ts @@ -1,4 +1,13 @@ +import { IRequestRequirement } from '../interfaces/requirement-request.interface'; + export class Requirement { + _requirementRequest: IRequestRequirement; seatsDriver: number; seatsPassenger: number; + + constructor(requirementRequest: IRequestRequirement, defaultSeats: number) { + this._requirementRequest = requirementRequest; + this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats; + this.seatsPassenger = requirementRequest.seatsPassenger ?? 1; + } } diff --git a/src/modules/matcher/domain/entities/settings.ts b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts similarity index 61% rename from src/modules/matcher/domain/entities/settings.ts rename to src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts index 14d53b8..0b4709f 100644 --- a/src/modules/matcher/domain/entities/settings.ts +++ b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts @@ -1,14 +1,13 @@ -import { Georouter } from '../interfaces/georouter.interface'; +import { Algorithm } from '../dtos/algorithm.enum'; -export class Settings { +export interface IRequestAlgorithmSettings { algorithm: Algorithm; - restrict: boolean; + strict: boolean; remoteness: number; useProportion: boolean; proportion: number; useAzimuth: boolean; azimuthMargin: number; - maxDetourDurationRatio: number; maxDetourDistanceRatio: number; - georouter: Georouter; + maxDetourDurationRatio: number; } diff --git a/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts b/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts new file mode 100644 index 0000000..63eb724 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/default-algorithm-settings.type.ts @@ -0,0 +1,15 @@ +import { Algorithm } from '../dtos/algorithm.enum'; + +export type DefaultAlgorithmSettings = { + algorithm: Algorithm; + strict: boolean; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDistanceRatio: number; + maxDetourDurationRatio: number; + georouterType: string; + georouterUrl: string; +}; diff --git a/src/modules/matcher/domain/interfaces/default-params.type.ts b/src/modules/matcher/domain/interfaces/default-params.type.ts index abcbe30..f39bd3b 100644 --- a/src/modules/matcher/domain/interfaces/default-params.type.ts +++ b/src/modules/matcher/domain/interfaces/default-params.type.ts @@ -1,6 +1,10 @@ +import { DefaultAlgorithmSettings } from './default-algorithm-settings.type'; + export type IDefaultParams = { DEFAULT_IDENTIFIER: number; MARGIN_DURATION: number; VALIDITY_DURATION: number; DEFAULT_TIMEZONE: string; + DEFAULT_SEATS: number; + DEFAULT_ALGORITHM_SETTINGS: DefaultAlgorithmSettings; }; diff --git a/src/modules/matcher/domain/interfaces/requirement-request.interface.ts b/src/modules/matcher/domain/interfaces/requirement-request.interface.ts new file mode 100644 index 0000000..61e5900 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/requirement-request.interface.ts @@ -0,0 +1,4 @@ +export interface IRequestRequirement { + seatsDriver?: number; + seatsPassenger?: number; +} diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index 2d798ea..c873f13 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -3,7 +3,7 @@ import { Geography } from '../domain/entities/geography'; import { Person } from '../domain/entities/person'; import { Requirement } from '../domain/entities/requirement'; import { Role } from '../domain/entities/role.enum'; -import { Settings } from '../domain/entities/settings'; +import { AlgorithmSettings } from '../domain/entities/algorithm-settings'; import { Time } from '../domain/entities/time'; import { IDefaultParams } from '../domain/interfaces/default-params.type'; @@ -16,7 +16,7 @@ export class MatchQuery { geography: Geography; exclusions: Array; requirement: Requirement; - settings: Settings; + algorithmSettings: AlgorithmSettings; constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { this._matchRequest = matchRequest; @@ -25,15 +25,11 @@ export class MatchQuery { this._setRoles(); this._setTime(); this._setGeography(); - this._initialize(); + this._setRequirement(); + this._setAlgorithmSettings(); this._setExclusions(); } - _initialize() { - this.requirement = new Requirement(); - this.settings = new Settings(); - } - _setPerson() { this.person = new Person( this._matchRequest, @@ -67,6 +63,21 @@ export class MatchQuery { this.geography.init(); } + _setRequirement() { + this.requirement = new Requirement( + this._matchRequest, + this._defaultParams.DEFAULT_SEATS, + ); + } + + _setAlgorithmSettings() { + this.algorithmSettings = new AlgorithmSettings( + this._matchRequest, + this._defaultParams.DEFAULT_ALGORITHM_SETTINGS, + this.time.frequency, + ); + } + _setExclusions() { this.exclusions = []; if (this._matchRequest.identifier) diff --git a/src/modules/matcher/tests/unit/match.query.spec.ts b/src/modules/matcher/tests/unit/match.query.spec.ts index 3ad22d3..ef5c883 100644 --- a/src/modules/matcher/tests/unit/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/match.query.spec.ts @@ -1,5 +1,7 @@ +import { Algorithm } from '../../domain/dtos/algorithm.enum'; import { MatchRequest } from '../../domain/dtos/match.request'; import { Role } from '../../domain/entities/role.enum'; +import { TimingFrequency } from '../../domain/entities/timing'; import { IDefaultParams } from '../../domain/interfaces/default-params.type'; import { MatchQuery } from '../../queries/match.query'; @@ -8,6 +10,20 @@ const defaultParams: IDefaultParams = { MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: Algorithm.CLASSIC, + strict: false, + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + georouterType: 'graphhopper', + georouterUrl: 'http://localhost', + }, }; describe('Match query', () => { @@ -28,28 +44,23 @@ describe('Match query', () => { expect(matchQuery).toBeDefined(); }); - describe('init', () => { - it('should create a query with excluded identifiers', () => { - const matchRequest: MatchRequest = new MatchRequest(); - matchRequest.departure = '2023-04-01 12:00'; - matchRequest.waypoints = [ - { - lat: 49.440041, - lon: 1.093912, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ]; - matchRequest.identifier = 125; - matchRequest.exclusions = [126, 127, 128]; - const matchQuery: MatchQuery = new MatchQuery( - matchRequest, - defaultParams, - ); - expect(matchQuery.exclusions.length).toBe(4); - }); + it('should create a query with excluded identifiers', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.identifier = 125; + matchRequest.exclusions = [126, 127, 128]; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.exclusions.length).toBe(4); }); it('should create a query with driver role only', () => { @@ -108,4 +119,60 @@ describe('Match query', () => { expect(matchQuery.roles).toContain(Role.PASSENGER); expect(matchQuery.roles).toContain(Role.DRIVER); }); + + it('should create a query with number of seats modified', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.seatsDriver = 1; + matchRequest.seatsPassenger = 2; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.requirement.seatsDriver).toBe(1); + expect(matchQuery.requirement.seatsPassenger).toBe(2); + }); + + it('should create a query with modified algorithm settings', () => { + const matchRequest: MatchRequest = new MatchRequest(); + matchRequest.departure = '2023-04-01 12:00'; + matchRequest.waypoints = [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ]; + matchRequest.algorithm = Algorithm.CLASSIC; + matchRequest.strict = true; + matchRequest.useProportion = true; + matchRequest.proportion = 0.45; + matchRequest.useAzimuth = true; + matchRequest.azimuthMargin = 15; + matchRequest.remoteness = 20000; + matchRequest.maxDetourDistanceRatio = 0.41; + matchRequest.maxDetourDurationRatio = 0.42; + const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams); + expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC); + expect(matchQuery.algorithmSettings.restrict).toBe( + TimingFrequency.FREQUENCY_PUNCTUAL, + ); + expect(matchQuery.algorithmSettings.useProportion).toBeTruthy(); + expect(matchQuery.algorithmSettings.proportion).toBe(0.45); + expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy(); + expect(matchQuery.algorithmSettings.azimuthMargin).toBe(15); + expect(matchQuery.algorithmSettings.remoteness).toBe(20000); + expect(matchQuery.algorithmSettings.maxDetourDistanceRatio).toBe(0.41); + expect(matchQuery.algorithmSettings.maxDetourDurationRatio).toBe(0.42); + }); }); diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index 5304b3d..c215eb2 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -7,6 +7,7 @@ import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; import { IDefaultParams } from '../../domain/interfaces/default-params.type'; +import { Algorithm } from '../../domain/dtos/algorithm.enum'; const mockAdRepository = {}; @@ -19,6 +20,20 @@ const defaultParams: IDefaultParams = { MARGIN_DURATION: 900, VALIDITY_DURATION: 365, DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: Algorithm.CLASSIC, + strict: false, + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + georouterType: 'graphhopper', + georouterUrl: 'http://localhost', + }, }; describe('MatchUseCase', () => {