diff --git a/.env.dist b/.env.dist index 412486b..dc8af99 100644 --- a/.env.dist +++ b/.env.dist @@ -13,7 +13,7 @@ DEFAULT_TIMEZONE=Europe/Paris # default number of seats proposed as driver DEFAULT_SEATS=3 # algorithm type -ALGORITHM=classic +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 @@ -38,7 +38,6 @@ VALIDITY_DURATION=365 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/package.json b/package.json index b2265f2..43e918b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose", "test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'", "test:cov": "jest --testPathPattern 'tests/unit/' --coverage", + "test:cov:watch": "jest --testPathPattern 'tests/unit/' --coverage --watch", "test:e2e": "jest --config ./test/jest-e2e.json", "generate": "docker exec v3-matcher-api sh -c 'npx prisma generate'", "migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'", diff --git a/src/modules/matcher/adapters/secondaries/geodesic.ts b/src/modules/matcher/adapters/secondaries/geodesic.ts index f2a9642..3743ac6 100644 --- a/src/modules/matcher/adapters/secondaries/geodesic.ts +++ b/src/modules/matcher/adapters/secondaries/geodesic.ts @@ -4,10 +4,10 @@ import { Geodesic, GeodesicClass } from 'geographiclib-geodesic'; @Injectable() export class MatcherGeodesic implements IGeodesic { - _geod: GeodesicClass; + private geod: GeodesicClass; constructor() { - this._geod = Geodesic.WGS84; + this.geod = Geodesic.WGS84; } inverse = ( @@ -16,7 +16,7 @@ export class MatcherGeodesic implements IGeodesic { lon2: number, lat2: number, ): { azimuth: number; distance: number } => { - const { azi2: azimuth, s12: distance } = this._geod.Inverse( + const { azi2: azimuth, s12: distance } = this.geod.Inverse( lat1, lon1, lat2, diff --git a/src/modules/matcher/adapters/secondaries/georouter-creator.ts b/src/modules/matcher/adapters/secondaries/georouter-creator.ts index 379920a..5589e7a 100644 --- a/src/modules/matcher/adapters/secondaries/georouter-creator.ts +++ b/src/modules/matcher/adapters/secondaries/georouter-creator.ts @@ -4,6 +4,10 @@ import { IGeorouter } from '../../domain/interfaces/georouter.interface'; import { GraphhopperGeorouter } from './graphhopper-georouter'; import { HttpService } from '@nestjs/axios'; import { MatcherGeodesic } from './geodesic'; +import { + MatcherException, + MatcherExceptionCode, +} from '../../exceptions/matcher.exception'; @Injectable() export class GeorouterCreator implements ICreateGeorouter { @@ -17,7 +21,10 @@ export class GeorouterCreator implements ICreateGeorouter { case 'graphhopper': return new GraphhopperGeorouter(url, this.httpService, this.geodesic); default: - throw new Error('Unknown geocoder'); + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'Unknown geocoder', + ); } }; } diff --git a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts index 26d2e23..33c79d8 100644 --- a/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts +++ b/src/modules/matcher/adapters/secondaries/graphhopper-georouter.ts @@ -9,82 +9,85 @@ import { IGeodesic } from '../../domain/interfaces/geodesic.interface'; import { NamedRoute } from '../../domain/entities/ecosystem/named-route'; import { Route } from '../../domain/entities/ecosystem/route'; import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point'; +import { + MatcherException, + MatcherExceptionCode, +} from '../../exceptions/matcher.exception'; @Injectable() export class GraphhopperGeorouter implements IGeorouter { - _url: string; - _urlArgs: Array; - _withTime: boolean; - _withPoints: boolean; - _withDistance: boolean; - _paths: Array; - _httpService: HttpService; - _geodesic: IGeodesic; + private url: string; + private urlArgs: Array; + private withTime: boolean; + private withPoints: boolean; + private withDistance: boolean; + private paths: Array; + private httpService: HttpService; + private geodesic: IGeodesic; constructor(url: string, httpService: HttpService, geodesic: IGeodesic) { - this._url = url + '/route?'; - this._httpService = httpService; - this._geodesic = geodesic; + this.url = url + '/route?'; + this.httpService = httpService; + this.geodesic = geodesic; } route = async ( paths: Array, settings: GeorouterSettings, ): Promise> => { - this._setDefaultUrlArgs(); - this._setWithTime(settings.withTime); - this._setWithPoints(settings.withPoints); - this._setWithDistance(settings.withDistance); - this._paths = paths; - return await this._getRoutes(); + this.setDefaultUrlArgs(); + this.setWithTime(settings.withTime); + this.setWithPoints(settings.withPoints); + this.setWithDistance(settings.withDistance); + this.paths = paths; + return await this.getRoutes(); }; - _setDefaultUrlArgs = (): void => { - this._urlArgs = [ - 'vehicle=car', - 'weighting=fastest', - 'points_encoded=false', - ]; + private setDefaultUrlArgs = (): void => { + this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false']; }; - _setWithTime = (withTime: boolean): void => { - this._withTime = withTime; + private setWithTime = (withTime: boolean): void => { + this.withTime = withTime; if (withTime) { - this._urlArgs.push('details=time'); + this.urlArgs.push('details=time'); } }; - _setWithPoints = (withPoints: boolean): void => { - this._withPoints = withPoints; + private setWithPoints = (withPoints: boolean): void => { + this.withPoints = withPoints; if (!withPoints) { - this._urlArgs.push('calc_points=false'); + this.urlArgs.push('calc_points=false'); } }; - _setWithDistance = (withDistance: boolean): void => { - this._withDistance = withDistance; + private setWithDistance = (withDistance: boolean): void => { + this.withDistance = withDistance; if (withDistance) { - this._urlArgs.push('instructions=true'); + this.urlArgs.push('instructions=true'); } else { - this._urlArgs.push('instructions=false'); + this.urlArgs.push('instructions=false'); } }; - _getRoutes = async (): Promise> => { + private getRoutes = async (): Promise> => { const routes = Promise.all( - this._paths.map(async (path) => { + this.paths.map(async (path) => { const url: string = [ - this._getUrl(), + this.getUrl(), '&point=', path.points .map((point) => [point.lat, point.lon].join()) .join('&point='), ].join(''); const route = await lastValueFrom( - this._httpService.get(url).pipe( - map((res) => (res.data ? this._createRoute(res) : undefined)), + this.httpService.get(url).pipe( + map((res) => (res.data ? this.createRoute(res) : undefined)), catchError((error: AxiosError) => { - throw new Error('Georouter unavailable : ' + error.message); + throw new MatcherException( + MatcherExceptionCode.INTERNAL, + 'Georouter unavailable : ' + error.message, + ); }), ), ); @@ -97,12 +100,14 @@ export class GraphhopperGeorouter implements IGeorouter { return routes; }; - _getUrl = (): string => { - return [this._url, this._urlArgs.join('&')].join(''); + private getUrl = (): string => { + return [this.url, this.urlArgs.join('&')].join(''); }; - _createRoute = (response: AxiosResponse): Route => { - const route = new Route(this._geodesic); + private createRoute = ( + response: AxiosResponse, + ): Route => { + const route = new Route(this.geodesic); if (response.data.paths && response.data.paths[0]) { const shortestPath = response.data.paths[0]; route.distance = shortestPath.distance ?? 0; @@ -124,7 +129,7 @@ export class GraphhopperGeorouter implements IGeorouter { if (shortestPath.instructions) instructions = shortestPath.instructions; route.setSpacetimePoints( - this._generateSpacetimePoints( + this.generateSpacetimePoints( shortestPath.points.coordinates, shortestPath.snapped_waypoints.coordinates, shortestPath.details.time, @@ -137,15 +142,15 @@ export class GraphhopperGeorouter implements IGeorouter { return route; }; - _generateSpacetimePoints = ( + private generateSpacetimePoints = ( points: Array>, snappedWaypoints: Array>, durations: Array>, instructions: Array, ): Array => { - const indices = this._getIndices(points, snappedWaypoints); - const times = this._getTimes(durations, indices); - const distances = this._getDistances(instructions, indices); + const indices = this.getIndices(points, snappedWaypoints); + const times = this.getTimes(durations, indices); + const distances = this.getDistances(instructions, indices); return indices.map( (index) => new SpacetimePoint( @@ -156,7 +161,7 @@ export class GraphhopperGeorouter implements IGeorouter { ); }; - _getIndices = ( + private getIndices = ( points: Array>, snappedWaypoints: Array>, ): Array => { @@ -188,7 +193,7 @@ export class GraphhopperGeorouter implements IGeorouter { .filter((element) => element.index == -1); for (const index in points) { for (const missedWaypoint of missedWaypoints) { - const inverse = this._geodesic.inverse( + const inverse = this.geodesic.inverse( missedWaypoint.waypoint[0], missedWaypoint.waypoint[1], points[index][0], @@ -206,7 +211,7 @@ export class GraphhopperGeorouter implements IGeorouter { return indices; }; - _getTimes = ( + private getTimes = ( durations: Array>, indices: Array, ): Array<{ index: number; duration: number }> => { @@ -256,7 +261,7 @@ export class GraphhopperGeorouter implements IGeorouter { return times; }; - _getDistances = ( + private getDistances = ( instructions: Array, indices: Array, ): Array<{ index: number; distance: number }> => { diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 14d7339..4cb1ff2 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -13,7 +13,7 @@ import { AutoMap } from '@automapper/classes'; import { Point } from '../types/point.type'; import { Schedule } from '../types/schedule.type'; import { MarginDurations } from '../types/margin-durations.type'; -import { Algorithm } from '../types/algorithm.enum'; +import { AlgorithmType } from '../types/algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface'; import { IRequestGeography } from '../interfaces/geography-request.interface'; @@ -89,9 +89,9 @@ export class MatchRequest strict: boolean; @IsOptional() - @IsEnum(Algorithm) + @IsEnum(AlgorithmType) @AutoMap() - algorithm: Algorithm; + algorithm: AlgorithmType; @IsOptional() @IsNumber() diff --git a/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts index aa12abf..a6d4963 100644 --- a/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts +++ b/src/modules/matcher/domain/entities/ecosystem/algorithm-settings.ts @@ -1,14 +1,14 @@ import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface'; import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type'; -import { Algorithm } from '../../types/algorithm.enum'; +import { AlgorithmType } from '../../types/algorithm.enum'; import { TimingFrequency } from '../../types/timing'; import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface'; import { IGeorouter } from '../../interfaces/georouter.interface'; export class AlgorithmSettings { - _algorithmSettingsRequest: IRequestAlgorithmSettings; - _strict: boolean; - algorithm: Algorithm; + private algorithmSettingsRequest: IRequestAlgorithmSettings; + private strict: boolean; + algorithmType: AlgorithmType; restrict: TimingFrequency; remoteness: number; useProportion: boolean; @@ -25,10 +25,10 @@ export class AlgorithmSettings { frequency: TimingFrequency, georouterCreator: ICreateGeorouter, ) { - this._algorithmSettingsRequest = algorithmSettingsRequest; - this.algorithm = + this.algorithmSettingsRequest = algorithmSettingsRequest; + this.algorithmType = algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm; - this._strict = + this.strict = algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict; this.remoteness = algorithmSettingsRequest.remoteness ? Math.abs(algorithmSettingsRequest.remoteness) @@ -55,7 +55,7 @@ export class AlgorithmSettings { defaultAlgorithmSettings.georouterType, defaultAlgorithmSettings.georouterUrl, ); - if (this._strict) { + if (this.strict) { this.restrict = frequency; } } diff --git a/src/modules/matcher/domain/entities/ecosystem/geography.ts b/src/modules/matcher/domain/entities/ecosystem/geography.ts index 592ef31..892e904 100644 --- a/src/modules/matcher/domain/entities/ecosystem/geography.ts +++ b/src/modules/matcher/domain/entities/ecosystem/geography.ts @@ -1,4 +1,7 @@ -import { MatcherException } from '../../../exceptions/matcher.exception'; +import { + MatcherException, + MatcherExceptionCode, +} from '../../../exceptions/matcher.exception'; import { IRequestGeography } from '../../interfaces/geography-request.interface'; import { PointType } from '../../types/geography.enum'; import { Point } from '../../types/point.type'; @@ -13,9 +16,9 @@ import { Step } from '../../types/step.enum'; import { Path } from '../../types/path.type'; export class Geography { - _geographyRequest: IRequestGeography; - _person: Person; - _points: Array; + private geographyRequest: IRequestGeography; + private person: Person; + private points: Array; originType: PointType; destinationType: PointType; timezones: Array; @@ -27,18 +30,18 @@ export class Geography { defaultTimezone: string, person: Person, ) { - this._geographyRequest = geographyRequest; - this._person = person; - this._points = []; + this.geographyRequest = geographyRequest; + this.person = person; + this.points = []; this.originType = undefined; this.destinationType = undefined; this.timezones = [defaultTimezone]; } init = (): void => { - this._validateWaypoints(); - this._setTimezones(); - this._setPointTypes(); + this.validateWaypoints(); + this.setTimezones(); + this.setPointTypes(); }; createRoutes = async ( @@ -49,14 +52,14 @@ export class Geography { let passengerWaypoints: Array = []; const paths: Array = []; if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) { - if (this._points.length == 2) { + if (this.points.length == 2) { // 2 points => same route for driver and passenger const commonPath: Path = { key: RouteKey.COMMON, - points: this._points, + points: this.points, }; - driverWaypoints = this._createWaypoints(commonPath.points, Role.DRIVER); - passengerWaypoints = this._createWaypoints( + driverWaypoints = this.createWaypoints(commonPath.points, Role.DRIVER); + passengerWaypoints = this.createWaypoints( commonPath.points, Role.PASSENGER, ); @@ -64,14 +67,14 @@ export class Geography { } else { const driverPath: Path = { key: RouteKey.DRIVER, - points: this._points, + points: this.points, }; - driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER); + driverWaypoints = this.createWaypoints(driverPath.points, Role.DRIVER); const passengerPath: Path = { key: RouteKey.PASSENGER, - points: [this._points[0], this._points[this._points.length - 1]], + points: [this.points[0], this.points[this.points.length - 1]], }; - passengerWaypoints = this._createWaypoints( + passengerWaypoints = this.createWaypoints( passengerPath.points, Role.PASSENGER, ); @@ -80,16 +83,16 @@ export class Geography { } else if (roles.includes(Role.DRIVER)) { const driverPath: Path = { key: RouteKey.DRIVER, - points: this._points, + points: this.points, }; - driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER); + driverWaypoints = this.createWaypoints(driverPath.points, Role.DRIVER); paths.push(driverPath); } else if (roles.includes(Role.PASSENGER)) { const passengerPath: Path = { key: RouteKey.PASSENGER, - points: [this._points[0], this._points[this._points.length - 1]], + points: [this.points[0], this.points[this.points.length - 1]], }; - passengerWaypoints = this._createWaypoints( + passengerWaypoints = this.createWaypoints( passengerPath.points, Role.PASSENGER, ); @@ -125,55 +128,61 @@ export class Geography { } }; - _validateWaypoints = (): void => { - if (this._geographyRequest.waypoints.length < 2) { - throw new MatcherException(3, 'At least 2 waypoints are required'); + private validateWaypoints = (): void => { + if (this.geographyRequest.waypoints.length < 2) { + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'At least 2 waypoints are required', + ); } - this._geographyRequest.waypoints.map((point) => { - if (!this._isValidPoint(point)) { + this.geographyRequest.waypoints.map((point) => { + if (!this.isValidPoint(point)) { throw new MatcherException( - 3, + MatcherExceptionCode.INVALID_ARGUMENT, `Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`, ); } - this._points.push(point); + this.points.push(point); }); }; - _setTimezones = (): void => { + private setTimezones = (): void => { this.timezones = find( - this._geographyRequest.waypoints[0].lat, - this._geographyRequest.waypoints[0].lon, + this.geographyRequest.waypoints[0].lat, + this.geographyRequest.waypoints[0].lon, ); }; - _setPointTypes = (): void => { + private setPointTypes = (): void => { this.originType = - this._geographyRequest.waypoints[0].type ?? PointType.OTHER; + this.geographyRequest.waypoints[0].type ?? PointType.OTHER; this.destinationType = - this._geographyRequest.waypoints[ - this._geographyRequest.waypoints.length - 1 + this.geographyRequest.waypoints[ + this.geographyRequest.waypoints.length - 1 ].type ?? PointType.OTHER; }; - _isValidPoint = (point: Point): boolean => - this._isValidLongitude(point.lon) && this._isValidLatitude(point.lat); + private isValidPoint = (point: Point): boolean => + this.isValidLongitude(point.lon) && this.isValidLatitude(point.lat); - _isValidLongitude = (longitude: number): boolean => + private isValidLongitude = (longitude: number): boolean => longitude >= -180 && longitude <= 180; - _isValidLatitude = (latitude: number): boolean => + private isValidLatitude = (latitude: number): boolean => latitude >= -90 && latitude <= 90; - _createWaypoints = (points: Array, role: Role): Array => { + private createWaypoints = ( + points: Array, + role: Role, + ): Array => { return points.map((point, index) => { const waypoint = new Waypoint(point); if (index == 0) { - waypoint.addActor(new Actor(this._person, role, Step.START)); + waypoint.addActor(new Actor(this.person, role, Step.START)); } else if (index == points.length - 1) { - waypoint.addActor(new Actor(this._person, role, Step.FINISH)); + waypoint.addActor(new Actor(this.person, role, Step.FINISH)); } else { - waypoint.addActor(new Actor(this._person, role, Step.INTERMEDIATE)); + waypoint.addActor(new Actor(this.person, role, Step.INTERMEDIATE)); } return waypoint; }); diff --git a/src/modules/matcher/domain/entities/ecosystem/person.ts b/src/modules/matcher/domain/entities/ecosystem/person.ts index 7340d07..c6baa02 100644 --- a/src/modules/matcher/domain/entities/ecosystem/person.ts +++ b/src/modules/matcher/domain/entities/ecosystem/person.ts @@ -1,9 +1,9 @@ import { IRequestPerson } from '../../interfaces/person-request.interface'; export class Person { - _personRequest: IRequestPerson; - _defaultIdentifier: number; - _defaultMarginDuration: number; + private personRequest: IRequestPerson; + private defaultIdentifier: number; + private defaultMarginDuration: number; identifier: number; marginDurations: Array; @@ -12,23 +12,21 @@ export class Person { defaultIdentifier: number, defaultMarginDuration: number, ) { - this._personRequest = personRequest; - this._defaultIdentifier = defaultIdentifier; - this._defaultMarginDuration = defaultMarginDuration; + this.personRequest = personRequest; + this.defaultIdentifier = defaultIdentifier; + this.defaultMarginDuration = defaultMarginDuration; } init = (): void => { - this.setIdentifier( - this._personRequest.identifier ?? this._defaultIdentifier, - ); + this.setIdentifier(this.personRequest.identifier ?? this.defaultIdentifier); this.setMarginDurations([ - this._defaultMarginDuration, - this._defaultMarginDuration, - this._defaultMarginDuration, - this._defaultMarginDuration, - this._defaultMarginDuration, - this._defaultMarginDuration, - this._defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, + this.defaultMarginDuration, ]); }; diff --git a/src/modules/matcher/domain/entities/ecosystem/requirement.ts b/src/modules/matcher/domain/entities/ecosystem/requirement.ts index 40db4c6..7100667 100644 --- a/src/modules/matcher/domain/entities/ecosystem/requirement.ts +++ b/src/modules/matcher/domain/entities/ecosystem/requirement.ts @@ -1,12 +1,12 @@ import { IRequestRequirement } from '../../interfaces/requirement-request.interface'; export class Requirement { - _requirementRequest: IRequestRequirement; + private requirementRequest: IRequestRequirement; seatsDriver: number; seatsPassenger: number; constructor(requirementRequest: IRequestRequirement, defaultSeats: number) { - this._requirementRequest = requirementRequest; + this.requirementRequest = requirementRequest; this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats; this.seatsPassenger = requirementRequest.seatsPassenger ?? 1; } diff --git a/src/modules/matcher/domain/entities/ecosystem/route.ts b/src/modules/matcher/domain/entities/ecosystem/route.ts index d468187..d2b1238 100644 --- a/src/modules/matcher/domain/entities/ecosystem/route.ts +++ b/src/modules/matcher/domain/entities/ecosystem/route.ts @@ -12,7 +12,7 @@ export class Route { waypoints: Array; points: Array; spacetimePoints: Array; - _geodesic: IGeodesic; + private geodesic: IGeodesic; constructor(geodesic: IGeodesic) { this.distance = undefined; @@ -23,25 +23,25 @@ export class Route { this.waypoints = []; this.points = []; this.spacetimePoints = []; - this._geodesic = geodesic; + this.geodesic = geodesic; } setWaypoints = (waypoints: Array): void => { this.waypoints = waypoints; - this._setAzimuth(waypoints.map((waypoint) => waypoint.point)); + this.setAzimuth(waypoints.map((waypoint) => waypoint.point)); }; setPoints = (points: Array): void => { this.points = points; - this._setAzimuth(points); + this.setAzimuth(points); }; setSpacetimePoints = (spacetimePoints: Array): void => { this.spacetimePoints = spacetimePoints; }; - _setAzimuth = (points: Array): void => { - const inverse = this._geodesic.inverse( + private setAzimuth = (points: Array): void => { + const inverse = this.geodesic.inverse( points[0].lon, points[0].lat, points[points.length - 1].lon, diff --git a/src/modules/matcher/domain/entities/ecosystem/time.ts b/src/modules/matcher/domain/entities/ecosystem/time.ts index c4a39c5..183a69a 100644 --- a/src/modules/matcher/domain/entities/ecosystem/time.ts +++ b/src/modules/matcher/domain/entities/ecosystem/time.ts @@ -1,13 +1,16 @@ -import { MatcherException } from '../../../exceptions/matcher.exception'; +import { + MatcherException, + MatcherExceptionCode, +} from '../../../exceptions/matcher.exception'; import { MarginDurations } from '../../types/margin-durations.type'; import { IRequestTime } from '../../interfaces/time-request.interface'; import { TimingDays, TimingFrequency, Days } from '../../types/timing'; import { Schedule } from '../../types/schedule.type'; export class Time { - _timeRequest: IRequestTime; - _defaultMarginDuration: number; - _defaultValidityDuration: number; + private timeRequest: IRequestTime; + private defaultMarginDuration: number; + private defaultValidityDuration: number; frequency: TimingFrequency; fromDate: Date; toDate: Date; @@ -19,9 +22,9 @@ export class Time { defaultMarginDuration: number, defaultValidityDuration: number, ) { - this._timeRequest = timeRequest; - this._defaultMarginDuration = defaultMarginDuration; - this._defaultValidityDuration = defaultValidityDuration; + this.timeRequest = timeRequest; + this.defaultMarginDuration = defaultMarginDuration; + this.defaultValidityDuration = defaultValidityDuration; this.schedule = {}; this.marginDurations = { mon: defaultMarginDuration, @@ -35,99 +38,120 @@ export class Time { } init = (): void => { - this._validateBaseDate(); - this._validatePunctualRequest(); - this._validateRecurrentRequest(); - this._setPunctualRequest(); - this._setRecurrentRequest(); - this._setMargindurations(); + this.validateBaseDate(); + this.validatePunctualRequest(); + this.validateRecurrentRequest(); + this.setPunctualRequest(); + this.setRecurrentRequest(); + this.setMargindurations(); }; - _validateBaseDate = (): void => { - if (!this._timeRequest.departure && !this._timeRequest.fromDate) { - throw new MatcherException(3, 'departure or fromDate is required'); + private validateBaseDate = (): void => { + if (!this.timeRequest.departure && !this.timeRequest.fromDate) { + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'departure or fromDate is required', + ); } }; - _validatePunctualRequest = (): void => { - if (this._timeRequest.departure) { - this.fromDate = this.toDate = new Date(this._timeRequest.departure); - if (!this._isDate(this.fromDate)) { - throw new MatcherException(3, 'Wrong departure date'); + private validatePunctualRequest = (): void => { + if (this.timeRequest.departure) { + this.fromDate = this.toDate = new Date(this.timeRequest.departure); + if (!this.isDate(this.fromDate)) { + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'Wrong departure date', + ); } } }; - _validateRecurrentRequest = (): void => { - if (this._timeRequest.fromDate) { - this.fromDate = new Date(this._timeRequest.fromDate); - if (!this._isDate(this.fromDate)) { - throw new MatcherException(3, 'Wrong fromDate'); + private validateRecurrentRequest = (): void => { + if (this.timeRequest.fromDate) { + this.fromDate = new Date(this.timeRequest.fromDate); + if (!this.isDate(this.fromDate)) { + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'Wrong fromDate', + ); } } - if (this._timeRequest.toDate) { - this.toDate = new Date(this._timeRequest.toDate); - if (!this._isDate(this.toDate)) { - throw new MatcherException(3, 'Wrong toDate'); + if (this.timeRequest.toDate) { + this.toDate = new Date(this.timeRequest.toDate); + if (!this.isDate(this.toDate)) { + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'Wrong toDate', + ); } if (this.toDate < this.fromDate) { - throw new MatcherException(3, 'toDate must be after fromDate'); + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'toDate must be after fromDate', + ); } } - if (this._timeRequest.fromDate) { - this._validateSchedule(); + if (this.timeRequest.fromDate) { + this.validateSchedule(); } }; - _validateSchedule = (): void => { - if (!this._timeRequest.schedule) { - throw new MatcherException(3, 'Schedule is required'); + private validateSchedule = (): void => { + if (!this.timeRequest.schedule) { + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'Schedule is required', + ); } if ( - !Object.keys(this._timeRequest.schedule).some((elem) => + !Object.keys(this.timeRequest.schedule).some((elem) => Days.includes(elem), ) ) { - throw new MatcherException(3, 'No valid day in the given schedule'); + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + 'No valid day in the given schedule', + ); } - Object.keys(this._timeRequest.schedule).map((day) => { - const time = new Date('1970-01-01 ' + this._timeRequest.schedule[day]); - if (!this._isDate(time)) { - throw new MatcherException(3, `Wrong time for ${day} in schedule`); + Object.keys(this.timeRequest.schedule).map((day) => { + const time = new Date('1970-01-01 ' + this.timeRequest.schedule[day]); + if (!this.isDate(time)) { + throw new MatcherException( + MatcherExceptionCode.INVALID_ARGUMENT, + `Wrong time for ${day} in schedule`, + ); } }); }; - _setPunctualRequest = (): void => { - if (this._timeRequest.departure) { + private setPunctualRequest = (): void => { + if (this.timeRequest.departure) { this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; this.schedule[TimingDays[this.fromDate.getDay()]] = this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); } }; - _setRecurrentRequest = (): void => { - if (this._timeRequest.fromDate) { + private setRecurrentRequest = (): void => { + if (this.timeRequest.fromDate) { this.frequency = TimingFrequency.FREQUENCY_RECURRENT; if (!this.toDate) { - this.toDate = this._addDays( - this.fromDate, - this._defaultValidityDuration, - ); + this.toDate = this.addDays(this.fromDate, this.defaultValidityDuration); } - this._setSchedule(); + this.setSchedule(); } }; - _setSchedule = (): void => { - Object.keys(this._timeRequest.schedule).map((day) => { - this.schedule[day] = this._timeRequest.schedule[day]; + private setSchedule = (): void => { + Object.keys(this.timeRequest.schedule).map((day) => { + this.schedule[day] = this.timeRequest.schedule[day]; }); }; - _setMargindurations = (): void => { - if (this._timeRequest.marginDuration) { - const duration = Math.abs(this._timeRequest.marginDuration); + private setMargindurations = (): void => { + if (this.timeRequest.marginDuration) { + const duration = Math.abs(this.timeRequest.marginDuration); this.marginDurations = { mon: duration, tue: duration, @@ -138,30 +162,30 @@ export class Time { sun: duration, }; } - if (this._timeRequest.marginDurations) { + if (this.timeRequest.marginDurations) { if ( - !Object.keys(this._timeRequest.marginDurations).some((elem) => + !Object.keys(this.timeRequest.marginDurations).some((elem) => Days.includes(elem), ) ) { throw new MatcherException( - 3, + MatcherExceptionCode.INVALID_ARGUMENT, 'No valid day in the given margin durations', ); } - Object.keys(this._timeRequest.marginDurations).map((day) => { + Object.keys(this.timeRequest.marginDurations).map((day) => { this.marginDurations[day] = Math.abs( - this._timeRequest.marginDurations[day], + this.timeRequest.marginDurations[day], ); }); } }; - _isDate = (date: Date): boolean => { + private isDate = (date: Date): boolean => { return date instanceof Date && isFinite(+date); }; - _addDays = (date: Date, days: number): Date => { + private addDays = (date: Date, days: number): Date => { const result = new Date(date); result.setDate(result.getDate() + days); return result; diff --git a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory-creator.ts b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory-creator.ts new file mode 100644 index 0000000..1bce748 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory-creator.ts @@ -0,0 +1,18 @@ +import { MatchQuery } from 'src/modules/matcher/queries/match.query'; +import { AlgorithmType } from '../../../types/algorithm.enum'; +import { AlgorithmFactory } from './algorithm-factory.abstract'; +import { ClassicAlgorithmFactory } from './classic'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AlgorithmFactoryCreator { + create = (matchQuery: MatchQuery): AlgorithmFactory => { + let algorithmFactory: AlgorithmFactory; + switch (matchQuery.algorithmSettings.algorithmType) { + case AlgorithmType.CLASSIC: + algorithmFactory = new ClassicAlgorithmFactory(matchQuery); + break; + } + return algorithmFactory; + }; +} diff --git a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts index 67206e8..0cc876d 100644 --- a/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/factory/algorithm-factory.abstract.ts @@ -1,15 +1,17 @@ import { MatchQuery } from 'src/modules/matcher/queries/match.query'; -import { Processor } from '../processor.abstract'; +import { Processor } from '../processor/processor.abstract'; import { Candidate } from '../candidate'; +import { Selector } from '../selector/selector.abstract'; export abstract class AlgorithmFactory { - _matchQuery: MatchQuery; - _candidates: Array; + protected matchQuery: MatchQuery; + private candidates: Array; constructor(matchQuery: MatchQuery) { - this._matchQuery = matchQuery; - this._candidates = []; + this.matchQuery = matchQuery; + this.candidates = []; } + abstract createSelector(): Selector; abstract createProcessors(): Array; } diff --git a/src/modules/matcher/domain/entities/engine/factory/classic.ts b/src/modules/matcher/domain/entities/engine/factory/classic.ts index 77a2d04..54880b0 100644 --- a/src/modules/matcher/domain/entities/engine/factory/classic.ts +++ b/src/modules/matcher/domain/entities/engine/factory/classic.ts @@ -1,9 +1,21 @@ import { AlgorithmFactory } from './algorithm-factory.abstract'; -import { Processor } from '../processor.abstract'; import { ClassicWaypointsCompleter } from '../processor/completer/classic-waypoint.completer.processor'; +import { RouteCompleter } from '../processor/completer/route.completer.processor'; +import { ClassicGeoFilter } from '../processor/filter/geofilter/classic.filter.processor'; +import { JourneyCompleter } from '../processor/completer/journey.completer.processor'; +import { ClassicTimeFilter } from '../processor/filter/timefilter/classic.filter.processor'; +import { Processor } from '../processor/processor.abstract'; +import { Selector } from '../selector/selector.abstract'; +import { ClassicSelector } from '../selector/classic.selector'; export class ClassicAlgorithmFactory extends AlgorithmFactory { - createProcessors(): Array { - return [new ClassicWaypointsCompleter(this._matchQuery)]; - } + createSelector = (): Selector => new ClassicSelector(this.matchQuery); + createProcessors = (): Array => [ + new ClassicWaypointsCompleter(this.matchQuery), + new RouteCompleter(this.matchQuery, true, true, true), + new ClassicGeoFilter(this.matchQuery), + new RouteCompleter(this.matchQuery), + new JourneyCompleter(this.matchQuery), + new ClassicTimeFilter(this.matchQuery), + ]; } diff --git a/src/modules/matcher/domain/entities/engine/matcher.ts b/src/modules/matcher/domain/entities/engine/matcher.ts index bc6da9c..48648af 100644 --- a/src/modules/matcher/domain/entities/engine/matcher.ts +++ b/src/modules/matcher/domain/entities/engine/matcher.ts @@ -1,21 +1,27 @@ +import { Injectable } from '@nestjs/common'; import { MatchQuery } from '../../../queries/match.query'; -import { Algorithm } from '../../types/algorithm.enum'; import { Match } from '../ecosystem/match'; import { Candidate } from './candidate'; import { AlgorithmFactory } from './factory/algorithm-factory.abstract'; -import { ClassicAlgorithmFactory } from './factory/classic'; +import { AlgorithmFactoryCreator } from './factory/algorithm-factory-creator'; +@Injectable() export class Matcher { - match = (matchQuery: MatchQuery): Array => { - let algorithm: AlgorithmFactory; - switch (matchQuery.algorithmSettings.algorithm) { - case Algorithm.CLASSIC: - algorithm = new ClassicAlgorithmFactory(matchQuery); - } - let candidates: Array = []; - for (const processor of algorithm.createProcessors()) { + constructor( + private readonly algorithmFactoryCreator: AlgorithmFactoryCreator, + ) {} + + match = async (matchQuery: MatchQuery): Promise> => { + const algorithmFactory: AlgorithmFactory = + this.algorithmFactoryCreator.create(matchQuery); + let candidates: Array = await algorithmFactory + .createSelector() + .select(); + for (const processor of algorithmFactory.createProcessors()) { candidates = processor.execute(candidates); } - return []; + const match = new Match(); + match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; + return [match]; }; } diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts b/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts index b55522a..baccba9 100644 --- a/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts +++ b/src/modules/matcher/domain/entities/engine/processor/completer/classic-waypoint.completer.processor.ts @@ -2,7 +2,7 @@ import { Candidate } from '../../candidate'; import { Completer } from './completer.abstract'; export class ClassicWaypointsCompleter extends Completer { - complete(candidates: Array): Array { - return []; - } + complete = (candidates: Array): Array => { + return candidates; + }; } diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts b/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts index 29f408d..e11bfee 100644 --- a/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/processor/completer/completer.abstract.ts @@ -1,5 +1,5 @@ import { Candidate } from '../../candidate'; -import { Processor } from '../../processor.abstract'; +import { Processor } from '../processor.abstract'; export abstract class Completer extends Processor { execute = (candidates: Array): Array => diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/journey.completer.processor.ts b/src/modules/matcher/domain/entities/engine/processor/completer/journey.completer.processor.ts new file mode 100644 index 0000000..69042b9 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor/completer/journey.completer.processor.ts @@ -0,0 +1,8 @@ +import { Candidate } from '../../candidate'; +import { Completer } from './completer.abstract'; + +export class JourneyCompleter extends Completer { + complete = (candidates: Array): Array => { + return candidates; + }; +} diff --git a/src/modules/matcher/domain/entities/engine/processor/completer/route.completer.processor.ts b/src/modules/matcher/domain/entities/engine/processor/completer/route.completer.processor.ts new file mode 100644 index 0000000..582bc03 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor/completer/route.completer.processor.ts @@ -0,0 +1,25 @@ +import { MatchQuery } from 'src/modules/matcher/queries/match.query'; +import { Candidate } from '../../candidate'; +import { Completer } from './completer.abstract'; + +export class RouteCompleter extends Completer { + private withPoints: boolean; + private withTime: boolean; + private withDistance: boolean; + + constructor( + matchQuery: MatchQuery, + withPoints = false, + withTime = false, + withDistance = false, + ) { + super(matchQuery); + this.withPoints = withPoints; + this.withTime = withTime; + this.withDistance = withDistance; + } + + complete = (candidates: Array): Array => { + return candidates; + }; +} diff --git a/src/modules/matcher/domain/entities/engine/processor/filter/filter.abstract.ts b/src/modules/matcher/domain/entities/engine/processor/filter/filter.abstract.ts new file mode 100644 index 0000000..87cd490 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor/filter/filter.abstract.ts @@ -0,0 +1,9 @@ +import { Candidate } from '../../candidate'; +import { Processor } from '../processor.abstract'; + +export abstract class Filter extends Processor { + execute = (candidates: Array): Array => + this.filter(candidates); + + abstract filter(candidates: Array): Array; +} diff --git a/src/modules/matcher/domain/entities/engine/processor/filter/geofilter/classic.filter.processor.ts b/src/modules/matcher/domain/entities/engine/processor/filter/geofilter/classic.filter.processor.ts new file mode 100644 index 0000000..dc0dc66 --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor/filter/geofilter/classic.filter.processor.ts @@ -0,0 +1,8 @@ +import { Candidate } from '../../../candidate'; +import { Filter } from '../filter.abstract'; + +export class ClassicGeoFilter extends Filter { + filter = (candidates: Array): Array => { + return candidates; + }; +} diff --git a/src/modules/matcher/domain/entities/engine/processor/filter/timefilter/classic.filter.processor.ts b/src/modules/matcher/domain/entities/engine/processor/filter/timefilter/classic.filter.processor.ts new file mode 100644 index 0000000..b69c32e --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/processor/filter/timefilter/classic.filter.processor.ts @@ -0,0 +1,8 @@ +import { Candidate } from '../../../candidate'; +import { Filter } from '../filter.abstract'; + +export class ClassicTimeFilter extends Filter { + filter = (candidates: Array): Array => { + return candidates; + }; +} diff --git a/src/modules/matcher/domain/entities/engine/processor.abstract.ts b/src/modules/matcher/domain/entities/engine/processor/processor.abstract.ts similarity index 66% rename from src/modules/matcher/domain/entities/engine/processor.abstract.ts rename to src/modules/matcher/domain/entities/engine/processor/processor.abstract.ts index c5df1a6..eee4c0c 100644 --- a/src/modules/matcher/domain/entities/engine/processor.abstract.ts +++ b/src/modules/matcher/domain/entities/engine/processor/processor.abstract.ts @@ -1,11 +1,11 @@ import { MatchQuery } from 'src/modules/matcher/queries/match.query'; -import { Candidate } from './candidate'; +import { Candidate } from '../candidate'; export abstract class Processor { - _matchQuery: MatchQuery; + private matchQuery: MatchQuery; constructor(matchQuery: MatchQuery) { - this._matchQuery = matchQuery; + this.matchQuery = matchQuery; } abstract execute(candidates: Array): Array; diff --git a/src/modules/matcher/domain/entities/engine/selector/classic.selector.ts b/src/modules/matcher/domain/entities/engine/selector/classic.selector.ts new file mode 100644 index 0000000..e87403c --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/selector/classic.selector.ts @@ -0,0 +1,8 @@ +import { Candidate } from '../candidate'; +import { Selector } from './selector.abstract'; + +export class ClassicSelector extends Selector { + select = async (): Promise> => { + return []; + }; +} diff --git a/src/modules/matcher/domain/entities/engine/selector/selector.abstract.ts b/src/modules/matcher/domain/entities/engine/selector/selector.abstract.ts new file mode 100644 index 0000000..b2b722e --- /dev/null +++ b/src/modules/matcher/domain/entities/engine/selector/selector.abstract.ts @@ -0,0 +1,12 @@ +import { MatchQuery } from 'src/modules/matcher/queries/match.query'; +import { Candidate } from '../candidate'; + +export abstract class Selector { + private matchQuery: MatchQuery; + + constructor(matchQuery: MatchQuery) { + this.matchQuery = matchQuery; + } + + abstract select(): Promise>; +} diff --git a/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts index 3ab0de8..484ec15 100644 --- a/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/algorithm-settings-request.interface.ts @@ -1,7 +1,7 @@ -import { Algorithm } from '../types/algorithm.enum'; +import { AlgorithmType } from '../types/algorithm.enum'; export interface IRequestAlgorithmSettings { - algorithm: Algorithm; + algorithm: AlgorithmType; strict: boolean; remoteness: number; useProportion: boolean; diff --git a/src/modules/matcher/domain/types/algorithm.enum.ts b/src/modules/matcher/domain/types/algorithm.enum.ts index 0ed0cbc..52f14bd 100644 --- a/src/modules/matcher/domain/types/algorithm.enum.ts +++ b/src/modules/matcher/domain/types/algorithm.enum.ts @@ -1,3 +1,3 @@ -export enum Algorithm { +export enum AlgorithmType { CLASSIC = 'CLASSIC', } diff --git a/src/modules/matcher/domain/types/default-algorithm-settings.type.ts b/src/modules/matcher/domain/types/default-algorithm-settings.type.ts index 89c0c93..a9edb47 100644 --- a/src/modules/matcher/domain/types/default-algorithm-settings.type.ts +++ b/src/modules/matcher/domain/types/default-algorithm-settings.type.ts @@ -1,7 +1,7 @@ -import { Algorithm } from './algorithm.enum'; +import { AlgorithmType } from './algorithm.enum'; export type DefaultAlgorithmSettings = { - algorithm: Algorithm; + algorithm: AlgorithmType; strict: boolean; remoteness: number; useProportion: boolean; diff --git a/src/modules/matcher/domain/usecases/match.usecase.ts b/src/modules/matcher/domain/usecases/match.usecase.ts index 44ce17b..fbb6952 100644 --- a/src/modules/matcher/domain/usecases/match.usecase.ts +++ b/src/modules/matcher/domain/usecases/match.usecase.ts @@ -3,64 +3,25 @@ import { InjectMapper } from '@automapper/nestjs'; import { QueryHandler } from '@nestjs/cqrs'; import { Messager } from '../../adapters/secondaries/messager'; import { MatchQuery } from '../../queries/match.query'; -import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { Match } from '../entities/ecosystem/match'; import { ICollection } from '../../../database/src/interfaces/collection.interface'; +import { Matcher } from '../entities/engine/matcher'; @QueryHandler(MatchQuery) export class MatchUseCase { constructor( - private readonly _repository: AdRepository, + private readonly _matcher: Matcher, private readonly _messager: Messager, @InjectMapper() private readonly _mapper: Mapper, ) {} execute = async (matchQuery: MatchQuery): Promise> => { try { - // const paths = []; - // for (let i = 0; i < 1; i++) { - // paths.push({ - // key: 'route' + i, - // points: [ - // { - // lat: 48.110899, - // lon: -1.68365, - // }, - // { - // lat: 48.131105, - // lon: -1.690067, - // }, - // { - // lat: 48.534769, - // lon: -1.894032, - // }, - // { - // lat: 48.56516, - // lon: -1.923553, - // }, - // { - // lat: 48.622813, - // lon: -1.997177, - // }, - // { - // lat: 48.67846, - // lon: -1.8554, - // }, - // ], - // }); - // } - // const routes = await matchQuery.algorithmSettings.georouter.route(paths, { - // withDistance: false, - // withPoints: true, - // withTime: true, - // }); - // routes.map((route) => console.log(route.route.spacetimePoints)); - const match = new Match(); - match.uuid = 'e23f9725-2c19-49a0-9ef6-17d8b9a5ec85'; + const data: Array = await this._matcher.match(matchQuery); this._messager.publish('matcher.match', 'match !'); return { - data: [match], - total: 1, + data, + total: data.length, }; } catch (error) { const err: Error = error; @@ -75,3 +36,42 @@ export class MatchUseCase { } }; } + +// const paths = []; +// for (let i = 0; i < 1; i++) { +// paths.push({ +// key: 'route' + i, +// points: [ +// { +// lat: 48.110899, +// lon: -1.68365, +// }, +// { +// lat: 48.131105, +// lon: -1.690067, +// }, +// { +// lat: 48.534769, +// lon: -1.894032, +// }, +// { +// lat: 48.56516, +// lon: -1.923553, +// }, +// { +// lat: 48.622813, +// lon: -1.997177, +// }, +// { +// lat: 48.67846, +// lon: -1.8554, +// }, +// ], +// }); +// } +// const routes = await matchQuery.algorithmSettings.georouter.route(paths, { +// withDistance: false, +// withPoints: true, +// withTime: true, +// }); +// routes.map((route) => console.log(route.route.spacetimePoints)); diff --git a/src/modules/matcher/exceptions/matcher.exception.ts b/src/modules/matcher/exceptions/matcher.exception.ts index c72c694..af70214 100644 --- a/src/modules/matcher/exceptions/matcher.exception.ts +++ b/src/modules/matcher/exceptions/matcher.exception.ts @@ -11,3 +11,23 @@ export class MatcherException implements Error { return this._code; } } + +export enum MatcherExceptionCode { + OK = 0, + CANCELLED = 1, + UNKNOWN = 2, + INVALID_ARGUMENT = 3, + DEADLINE_EXCEEDED = 4, + NOT_FOUND = 5, + ALREADY_EXISTS = 6, + PERMISSION_DENIED = 7, + RESOURCE_EXHAUSTED = 8, + FAILED_PRECONDITION = 9, + ABORTED = 10, + OUT_OF_RANGE = 11, + UNIMPLEMENTED = 12, + INTERNAL = 13, + UNAVAILABLE = 14, + DATA_LOSS = 15, + UNAUTHENTICATED = 16, +} diff --git a/src/modules/matcher/matcher.module.ts b/src/modules/matcher/matcher.module.ts index 7173746..aacb6b2 100644 --- a/src/modules/matcher/matcher.module.ts +++ b/src/modules/matcher/matcher.module.ts @@ -15,6 +15,8 @@ import { DefaultParamsProvider } from './adapters/secondaries/default-params.pro import { GeorouterCreator } from './adapters/secondaries/georouter-creator'; import { HttpModule } from '@nestjs/axios'; import { MatcherGeodesic } from './adapters/secondaries/geodesic'; +import { Matcher } from './domain/entities/engine/matcher'; +import { AlgorithmFactoryCreator } from './domain/entities/engine/factory/algorithm-factory-creator'; @Module({ imports: [ @@ -57,6 +59,8 @@ import { MatcherGeodesic } from './adapters/secondaries/geodesic'; MatchUseCase, GeorouterCreator, MatcherGeodesic, + Matcher, + AlgorithmFactoryCreator, ], exports: [], }) diff --git a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts index e4d20a1..9f93de6 100644 --- a/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts +++ b/src/modules/matcher/tests/unit/domain/ecosystem/geography.spec.ts @@ -107,7 +107,6 @@ describe('Geography entity', () => { person, ); geography.init(); - expect(geography._points.length).toBe(2); expect(geography.originType).toBe(PointType.LOCALITY); expect(geography.destinationType).toBe(PointType.LOCALITY); }); diff --git a/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts new file mode 100644 index 0000000..79bf1fc --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory-creator.spec.ts @@ -0,0 +1,61 @@ +import { AlgorithmFactoryCreator } from '../../../../domain/entities/engine/factory/algorithm-factory-creator'; +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { ClassicAlgorithmFactory } from '../../../../domain/entities/engine/factory/classic'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('AlgorithmFactoryCreator', () => { + it('should be defined', () => { + expect(new AlgorithmFactoryCreator()).toBeDefined(); + }); + + it('should create a classic algorithm factory', () => { + expect(new AlgorithmFactoryCreator().create(matchQuery)).toBeInstanceOf( + ClassicAlgorithmFactory, + ); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/algorithm-factory.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory.abstract.spec.ts new file mode 100644 index 0000000..7ec1886 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/algorithm-factory.abstract.spec.ts @@ -0,0 +1,78 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { AlgorithmFactory } from '../../../../domain/entities/engine/factory/algorithm-factory.abstract'; +import { Processor } from '../../../../domain/entities/engine/processor/processor.abstract'; +import { Selector } from '../../../../domain/entities/engine/selector/selector.abstract'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +class FakeSelector extends Selector { + select = (): Promise => { + return Promise.resolve([new Candidate()]); + }; +} + +class FakeProcessor extends Processor { + execute = (candidates: Candidate[]): Candidate[] => { + return candidates; + }; +} + +class FakeAlgorithmFactory extends AlgorithmFactory { + createSelector = (): Selector => { + return new FakeSelector(matchQuery); + }; + createProcessors = (): Processor[] => { + return [new FakeProcessor(matchQuery)]; + }; +} + +describe('AlgorithmFactory', () => { + it('should create an extended class', () => { + expect(new FakeAlgorithmFactory(matchQuery)).toBeDefined(); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-algorithm-factory.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-algorithm-factory.spec.ts new file mode 100644 index 0000000..45d8a31 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/classic-algorithm-factory.spec.ts @@ -0,0 +1,69 @@ +import { ClassicSelector } from '../../../../domain/entities/engine/selector/classic.selector'; +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { ClassicAlgorithmFactory } from '../../../../domain/entities/engine/factory/classic'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('ClassicAlgorithmFactory', () => { + it('should be defined', () => { + expect(new ClassicAlgorithmFactory(matchQuery)).toBeDefined(); + }); + + it('should create a classic selector', () => { + const classicAlgorithmFactory: ClassicAlgorithmFactory = + new ClassicAlgorithmFactory(matchQuery); + expect(classicAlgorithmFactory.createSelector()).toBeInstanceOf( + ClassicSelector, + ); + }); + + it('should create processors', () => { + const classicAlgorithmFactory: ClassicAlgorithmFactory = + new ClassicAlgorithmFactory(matchQuery); + expect(classicAlgorithmFactory.createProcessors().length).toBe(6); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-geo.filter.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-geo.filter.processor.spec.ts new file mode 100644 index 0000000..abd06b8 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/classic-geo.filter.processor.spec.ts @@ -0,0 +1,63 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { ClassicGeoFilter } from '../../../../domain/entities/engine/processor/filter/geofilter/classic.filter.processor'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('ClassicGeoFilter', () => { + it('should be defined', () => { + expect(new ClassicGeoFilter(matchQuery)).toBeDefined(); + }); + + it('should filter candidates', () => { + const candidates = [new Candidate(), new Candidate()]; + const classicWaypointCompleter: ClassicGeoFilter = new ClassicGeoFilter( + matchQuery, + ); + expect(classicWaypointCompleter.filter(candidates).length).toBe(2); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-time.filter.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-time.filter.processor.spec.ts new file mode 100644 index 0000000..fe92e70 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/classic-time.filter.processor.spec.ts @@ -0,0 +1,63 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { ClassicTimeFilter } from '../../../../domain/entities/engine/processor/filter/timefilter/classic.filter.processor'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('ClassicTimeFilter', () => { + it('should be defined', () => { + expect(new ClassicTimeFilter(matchQuery)).toBeDefined(); + }); + + it('should filter candidates', () => { + const candidates = [new Candidate(), new Candidate()]; + const classicWaypointCompleter: ClassicTimeFilter = new ClassicTimeFilter( + matchQuery, + ); + expect(classicWaypointCompleter.filter(candidates).length).toBe(2); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/classic-waypoint.completer.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic-waypoint.completer.processor.spec.ts new file mode 100644 index 0000000..500193e --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/classic-waypoint.completer.processor.spec.ts @@ -0,0 +1,62 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { ClassicWaypointsCompleter } from '../../../../domain/entities/engine/processor/completer/classic-waypoint.completer.processor'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('ClassicWaypointCompleter', () => { + it('should be defined', () => { + expect(new ClassicWaypointsCompleter(matchQuery)).toBeDefined(); + }); + + it('should complete candidates', () => { + const candidates = [new Candidate(), new Candidate()]; + const classicWaypointCompleter: ClassicWaypointsCompleter = + new ClassicWaypointsCompleter(matchQuery); + expect(classicWaypointCompleter.complete(candidates).length).toBe(2); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/classic.selector.spec.ts b/src/modules/matcher/tests/unit/domain/engine/classic.selector.spec.ts new file mode 100644 index 0000000..8eb2954 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/classic.selector.spec.ts @@ -0,0 +1,61 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { ClassicSelector } from '../../../../domain/entities/engine/selector/classic.selector'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('ClassicSelector', () => { + it('should be defined', () => { + expect(new ClassicSelector(matchQuery)).toBeDefined(); + }); + + it('should select candidates', async () => { + const classicSelector: ClassicSelector = new ClassicSelector(matchQuery); + const candidates: Candidate[] = await classicSelector.select(); + expect(candidates.length).toBe(0); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/completer.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/completer.abstract.spec.ts new file mode 100644 index 0000000..f94bbcf --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/completer.abstract.spec.ts @@ -0,0 +1,68 @@ +import { Completer } from '../../../../domain/entities/engine/processor/completer/completer.abstract'; +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +class FakeCompleter extends Completer { + complete = (candidates: Candidate[]): Candidate[] => { + return candidates; + }; +} + +describe('Completer', () => { + it('should create an extended class', () => { + expect(new FakeCompleter(matchQuery)).toBeDefined(); + }); + + it('should call complete method', () => { + const fakeCompleter: Completer = new FakeCompleter(matchQuery); + const completerSpy = jest.spyOn(fakeCompleter, 'complete'); + fakeCompleter.execute([new Candidate()]); + expect(completerSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/filter.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/filter.abstract.spec.ts new file mode 100644 index 0000000..dfb1e64 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/filter.abstract.spec.ts @@ -0,0 +1,68 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Filter } from '../../../../domain/entities/engine/processor/filter/filter.abstract'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +class FakeFilter extends Filter { + filter = (candidates: Candidate[]): Candidate[] => { + return candidates; + }; +} + +describe('Filter', () => { + it('should create an extended class', () => { + expect(new FakeFilter(matchQuery)).toBeDefined(); + }); + + it('should call complete method', () => { + const fakeFilter: Filter = new FakeFilter(matchQuery); + const filterSpy = jest.spyOn(fakeFilter, 'filter'); + fakeFilter.execute([new Candidate()]); + expect(filterSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/journey.completer.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/journey.completer.processor.spec.ts new file mode 100644 index 0000000..9eb9a58 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/journey.completer.processor.spec.ts @@ -0,0 +1,61 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { JourneyCompleter } from '../../../../domain/entities/engine/processor/completer/journey.completer.processor'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('JourneyCompleter', () => { + it('should be defined', () => { + expect(new JourneyCompleter(matchQuery)).toBeDefined(); + }); + + it('should complete candidates', () => { + const candidates = [new Candidate(), new Candidate()]; + const journeyCompleter: JourneyCompleter = new JourneyCompleter(matchQuery); + expect(journeyCompleter.complete(candidates).length).toBe(2); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts new file mode 100644 index 0000000..0f1aca7 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/matcher.spec.ts @@ -0,0 +1,73 @@ +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Matcher } from '../../../../domain/entities/engine/matcher'; + +const mockAlgorithmFactoryCreator = { + create: jest.fn().mockReturnValue({ + createSelector: jest.fn().mockReturnValue({ + select: jest.fn(), + }), + createProcessors: jest.fn().mockReturnValue([ + { + execute: jest.fn(), + }, + ]), + }), +}; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('Matcher', () => { + it('should be defined', () => { + expect(new Matcher(mockAlgorithmFactoryCreator)).toBeDefined(); + }); + + it('should return matches', async () => { + const matcher = new Matcher(mockAlgorithmFactoryCreator); + const matches = await matcher.match(matchQuery); + expect(matches.length).toBe(1); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/processor.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/processor.abstract.spec.ts new file mode 100644 index 0000000..66bfefb --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/processor.abstract.spec.ts @@ -0,0 +1,61 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { Processor } from '../../../../domain/entities/engine/processor/processor.abstract'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +class FakeProcessor extends Processor { + execute = (candidates: Candidate[]): Candidate[] => { + return candidates; + }; +} + +describe('Processor', () => { + it('should create an extended class', () => { + expect(new FakeProcessor(matchQuery)).toBeDefined(); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/route.completer.processor.spec.ts b/src/modules/matcher/tests/unit/domain/engine/route.completer.processor.spec.ts new file mode 100644 index 0000000..4863945 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/route.completer.processor.spec.ts @@ -0,0 +1,61 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { RouteCompleter } from '../../../../domain/entities/engine/processor/completer/route.completer.processor'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +describe('RouteCompleter', () => { + it('should be defined', () => { + expect(new RouteCompleter(matchQuery)).toBeDefined(); + }); + + it('should complete candidates', () => { + const candidates = [new Candidate(), new Candidate()]; + const routeCompleter: RouteCompleter = new RouteCompleter(matchQuery); + expect(routeCompleter.complete(candidates).length).toBe(2); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/engine/selector.abstract.spec.ts b/src/modules/matcher/tests/unit/domain/engine/selector.abstract.spec.ts new file mode 100644 index 0000000..01f9eb7 --- /dev/null +++ b/src/modules/matcher/tests/unit/domain/engine/selector.abstract.spec.ts @@ -0,0 +1,61 @@ +import { MatchRequest } from '../../../../domain/dtos/match.request'; +import { Candidate } from '../../../../domain/entities/engine/candidate'; +import { AlgorithmType } from '../../../../domain/types/algorithm.enum'; +import { IDefaultParams } from '../../../../domain/types/default-params.type'; +import { MatchQuery } from '../../../../queries/match.query'; +import { Selector } from '../../../../domain/entities/engine/selector/selector.abstract'; + +const mockGeorouterCreator = { + create: jest.fn().mockImplementation(), +}; + +const defaultParams: IDefaultParams = { + DEFAULT_IDENTIFIER: 0, + MARGIN_DURATION: 900, + VALIDITY_DURATION: 365, + DEFAULT_TIMEZONE: 'Europe/Paris', + DEFAULT_SEATS: 3, + DEFAULT_ALGORITHM_SETTINGS: { + algorithm: AlgorithmType.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', + }, +}; + +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, + }, +]; +const matchQuery: MatchQuery = new MatchQuery( + matchRequest, + defaultParams, + mockGeorouterCreator, +); + +class FakeSelector extends Selector { + select = (): Promise => { + return Promise.resolve([new Candidate()]); + }; +} + +describe('Selector', () => { + it('should create an extended class', () => { + expect(new FakeSelector(matchQuery)).toBeDefined(); + }); +}); diff --git a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts index 6de7ad9..c5e0fda 100644 --- a/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/domain/match.usecase.spec.ts @@ -3,13 +3,28 @@ import { Messager } from '../../../adapters/secondaries/messager'; import { MatchUseCase } from '../../../domain/usecases/match.usecase'; import { MatchRequest } from '../../../domain/dtos/match.request'; import { MatchQuery } from '../../../queries/match.query'; -import { AdRepository } from '../../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; import { IDefaultParams } from '../../../domain/types/default-params.type'; -import { Algorithm } from '../../../domain/types/algorithm.enum'; +import { AlgorithmType } from '../../../domain/types/algorithm.enum'; +import { Matcher } from '../../../domain/entities/engine/matcher'; +import { Match } from '../../../domain/entities/ecosystem/match'; +import { + MatcherException, + MatcherExceptionCode, +} from '../../../exceptions/matcher.exception'; -const mockAdRepository = {}; +const mockMatcher = { + match: jest + .fn() + .mockImplementationOnce(() => [new Match(), new Match(), new Match()]) + .mockImplementationOnce(() => { + throw new MatcherException( + MatcherExceptionCode.INTERNAL, + 'Something terrible happened !', + ); + }), +}; const mockMessager = { publish: jest.fn().mockImplementation(), @@ -26,7 +41,7 @@ const defaultParams: IDefaultParams = { DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: Algorithm.CLASSIC, + algorithm: AlgorithmType.CLASSIC, strict: false, remoteness: 15000, useProportion: true, @@ -40,6 +55,19 @@ const defaultParams: IDefaultParams = { }, }; +const matchRequest: MatchRequest = new MatchRequest(); +matchRequest.waypoints = [ + { + lon: 1.093912, + lat: 49.440041, + }, + { + lat: 50.630992, + lon: 3.045432, + }, +]; +matchRequest.departure = '2023-04-01 12:23:00'; + describe('MatchUseCase', () => { let matchUseCase: MatchUseCase; @@ -47,14 +75,14 @@ describe('MatchUseCase', () => { const module: TestingModule = await Test.createTestingModule({ imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], providers: [ - { - provide: AdRepository, - useValue: mockAdRepository, - }, { provide: Messager, useValue: mockMessager, }, + { + provide: Matcher, + useValue: mockMatcher, + }, MatchUseCase, ], }).compile(); @@ -68,22 +96,18 @@ describe('MatchUseCase', () => { describe('execute', () => { it('should return matches', async () => { - const matchRequest: MatchRequest = new MatchRequest(); - matchRequest.waypoints = [ - { - lon: 1.093912, - lat: 49.440041, - }, - { - lat: 50.630992, - lon: 3.045432, - }, - ]; - matchRequest.departure = '2023-04-01 12:23:00'; const matches = await matchUseCase.execute( new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), ); - expect(matches.total).toBe(1); + expect(matches.total).toBe(3); + }); + + it('should throw an exception when error occurs', async () => { + await expect( + matchUseCase.execute( + new MatchQuery(matchRequest, defaultParams, mockGeorouterCreator), + ), + ).rejects.toBeInstanceOf(MatcherException); }); }); }); diff --git a/src/modules/matcher/tests/unit/queries/match.query.spec.ts b/src/modules/matcher/tests/unit/queries/match.query.spec.ts index 8ed650b..f761bcf 100644 --- a/src/modules/matcher/tests/unit/queries/match.query.spec.ts +++ b/src/modules/matcher/tests/unit/queries/match.query.spec.ts @@ -3,7 +3,7 @@ import { Role } from '../../../domain/types/role.enum'; import { TimingFrequency } from '../../../domain/types/timing'; import { IDefaultParams } from '../../../domain/types/default-params.type'; import { MatchQuery } from '../../../queries/match.query'; -import { Algorithm } from '../../../domain/types/algorithm.enum'; +import { AlgorithmType } from '../../../domain/types/algorithm.enum'; const defaultParams: IDefaultParams = { DEFAULT_IDENTIFIER: 0, @@ -12,7 +12,7 @@ const defaultParams: IDefaultParams = { DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_SEATS: 3, DEFAULT_ALGORITHM_SETTINGS: { - algorithm: Algorithm.CLASSIC, + algorithm: AlgorithmType.CLASSIC, strict: false, remoteness: 15000, useProportion: true, @@ -181,7 +181,7 @@ describe('Match query', () => { lon: 3.045432, }, ]; - matchRequest.algorithm = Algorithm.CLASSIC; + matchRequest.algorithm = AlgorithmType.CLASSIC; matchRequest.strict = true; matchRequest.useProportion = true; matchRequest.proportion = 0.45; @@ -195,7 +195,9 @@ describe('Match query', () => { defaultParams, mockGeorouterCreator, ); - expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC); + expect(matchQuery.algorithmSettings.algorithmType).toBe( + AlgorithmType.CLASSIC, + ); expect(matchQuery.algorithmSettings.restrict).toBe( TimingFrequency.FREQUENCY_PUNCTUAL, );