diff --git a/package-lock.json b/package-lock.json index 3512f8a..0d2f171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "geo-tz": "^7.0.7", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -2157,6 +2158,37 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/babel__core": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", @@ -3005,6 +3037,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/array-source": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz", + "integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -4658,6 +4695,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-source": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz", + "integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==", + "dependencies": { + "stream-source": "0.3" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4877,6 +4922,62 @@ "node": ">=6.9.0" } }, + "node_modules/geo-tz": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-7.0.7.tgz", + "integrity": "sha512-Aq0sRSO1y4w62D5muRqzDmN4SWfFYnt703BLiqiHAvunlwsJs4qd3Fkl1pKSUa0bwuBmPFxIA8M1E+ilg2PSjw==", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "geobuf": "^3.0.2", + "pbf": "^3.2.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/geobuf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/geobuf/-/geobuf-3.0.2.tgz", + "integrity": "sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==", + "dependencies": { + "concat-stream": "^2.0.0", + "pbf": "^3.2.1", + "shapefile": "~0.6.6" + }, + "bin": { + "geobuf2json": "bin/geobuf2json", + "json2geobuf": "bin/json2geobuf", + "shp2geobuf": "bin/shp2geobuf" + } + }, + "node_modules/geobuf/node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/geobuf/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5088,7 +5189,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -6850,6 +6950,15 @@ "node": ">=12" } }, + "node_modules/path-source": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz", + "integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==", + "dependencies": { + "array-source": "0.0", + "file-source": "0.6" + } + }, "node_modules/path-to-regexp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", @@ -6864,6 +6973,18 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -7094,6 +7215,11 @@ "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7353,6 +7479,14 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -7660,6 +7794,28 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shapefile": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz", + "integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==", + "dependencies": { + "array-source": "0.0", + "commander": "2", + "path-source": "0.1", + "slice-source": "0.4", + "stream-source": "0.3", + "text-encoding": "^0.6.4" + }, + "bin": { + "dbf2json": "bin/dbf2json", + "shp2json": "bin/shp2json" + } + }, + "node_modules/shapefile/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7732,6 +7888,11 @@ "node": ">=8" } }, + "node_modules/slice-source": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz", + "integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==" + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -7800,6 +7961,11 @@ "node": ">= 0.8" } }, + "node_modules/stream-source": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz", + "integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -8076,6 +8242,12 @@ "node": ">=8" } }, + "node_modules/text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==", + "deprecated": "no longer maintained" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index f8e276e..a4896ab 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "cache-manager-ioredis-yet": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "geo-tz": "^7.0.7", "ioredis": "^5.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -101,6 +102,14 @@ "collectCoverageFrom": [ "**/*.(t|j)s" ], + "coveragePathIgnorePatterns": [ + ".controller.ts", + ".module.ts", + ".request.ts", + ".presenter.ts", + ".exception.ts", + "main.ts" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" } diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 3a9d736..0a96193 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -16,8 +16,11 @@ import { MarginDurations } from '../entities/margin-durations.type'; 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'; -export class MatchRequest implements IRequestTime, IRequestPerson { +export class MatchRequest + implements IRequestTime, IRequestPerson, IRequestGeography +{ @IsArray() @AutoMap() waypoints: Array; diff --git a/src/modules/matcher/domain/entities/actor.ts b/src/modules/matcher/domain/entities/actor.ts new file mode 100644 index 0000000..09e977b --- /dev/null +++ b/src/modules/matcher/domain/entities/actor.ts @@ -0,0 +1,9 @@ +import { Person } from './person'; +import { Role } from './role.enum'; +import { Step } from './step.enum'; + +export type Actor = { + person: Person; + role: Role; + step: Step; +}; diff --git a/src/modules/matcher/domain/entities/geography.enum.ts b/src/modules/matcher/domain/entities/geography.enum.ts new file mode 100644 index 0000000..e1e57a2 --- /dev/null +++ b/src/modules/matcher/domain/entities/geography.enum.ts @@ -0,0 +1,7 @@ +export enum PointType { + HOUSE_NUMBER = 'HOUSE_NUMBER', + STREET_ADDRESS = 'STREET_ADDRESS', + LOCALITY = 'LOCALITY', + VENUE = 'VENUE', + OTHER = 'OTHER', +} diff --git a/src/modules/matcher/domain/entities/geography.ts b/src/modules/matcher/domain/entities/geography.ts index 4cc5007..475f91b 100644 --- a/src/modules/matcher/domain/entities/geography.ts +++ b/src/modules/matcher/domain/entities/geography.ts @@ -1,11 +1,63 @@ +import { MatcherException } from '../../exceptions/matcher.exception'; +import { IRequestGeography } from '../interfaces/geography-request.interface'; +import { PointType } from './geography.enum'; import { Point } from './point.type'; import { Route } from './route'; +import { find } from 'geo-tz'; +import { Waypoint } from './waypoint'; export class Geography { - waypoints: Array; - originType: number; - destinationType: number; - timezone: string; + _geographyRequest: IRequestGeography; + waypoints: Array; + originType: PointType; + destinationType: PointType; + timeZones: Array; driverRoute: Route; passengerRoute: Route; + + constructor(geographyRequest: IRequestGeography) { + this._geographyRequest = geographyRequest; + this.waypoints = []; + this.originType = PointType.OTHER; + this.destinationType = PointType.OTHER; + } + + init() { + this._validateWaypoints(); + this._setTimeZones(); + } + + _validateWaypoints() { + if (this._geographyRequest.waypoints.length < 2) { + throw new MatcherException(3, 'At least 2 waypoints are required'); + } + this._geographyRequest.waypoints.map((point) => { + if (!this._isValidPoint(point)) { + throw new MatcherException( + 3, + `Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`, + ); + } + this.waypoints.push({ + point, + actors: [], + }); + }); + } + + _setTimeZones() { + this.timeZones = find( + this._geographyRequest.waypoints[0].lat, + this._geographyRequest.waypoints[0].lon, + ); + } + + _isValidPoint = (point: Point): boolean => + this._isValidLongitude(point.lon) && this._isValidLatitude(point.lat); + + _isValidLongitude = (longitude: number): boolean => + longitude >= -180 && longitude <= 180; + + _isValidLatitude = (latitude: number): boolean => + latitude >= -90 && latitude <= 90; } diff --git a/src/modules/matcher/domain/entities/step.enum.ts b/src/modules/matcher/domain/entities/step.enum.ts new file mode 100644 index 0000000..4b2fce4 --- /dev/null +++ b/src/modules/matcher/domain/entities/step.enum.ts @@ -0,0 +1,6 @@ +export enum Step { + START = 'start', + INTERMEDIATE = 'intermediate', + NEUTRAL = 'neutral', + FINISH = 'finish', +} diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index ef4b0bf..4669fc8 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -70,6 +70,9 @@ export class Time { if (!this._isDate(this.toDate)) { throw new MatcherException(3, 'Wrong toDate'); } + if (this.toDate < this.fromDate) { + throw new MatcherException(3, 'toDate must be after fromDate'); + } } if (this._timeRequest.fromDate) { this._validateSchedule(); diff --git a/src/modules/matcher/domain/entities/waypoint.ts b/src/modules/matcher/domain/entities/waypoint.ts new file mode 100644 index 0000000..62fe713 --- /dev/null +++ b/src/modules/matcher/domain/entities/waypoint.ts @@ -0,0 +1,7 @@ +import { Actor } from './actor'; +import { Point } from './point.type'; + +export type Waypoint = { + point: Point; + actors: Array; +}; diff --git a/src/modules/matcher/domain/interfaces/geography-request.interface.ts b/src/modules/matcher/domain/interfaces/geography-request.interface.ts new file mode 100644 index 0000000..79a18b3 --- /dev/null +++ b/src/modules/matcher/domain/interfaces/geography-request.interface.ts @@ -0,0 +1,5 @@ +import { Point } from '../entities/point.type'; + +export interface IRequestGeography { + waypoints: Array; +} diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index e8d9a44..6f35f7b 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -13,8 +13,8 @@ export class MatchQuery { person: Person; roles: Array; time: Time; - exclusions: Array; geography: Geography; + exclusions: Array; requirement: Requirement; settings: Settings; @@ -24,12 +24,12 @@ export class MatchQuery { this._setPerson(); this._setRoles(); this._setTime(); + this._setGeography(); this._initialize(); this._setExclusions(); } _initialize() { - this.geography = new Geography(); this.requirement = new Requirement(); this.settings = new Settings(); } @@ -59,6 +59,11 @@ export class MatchQuery { this.time.init(); } + _setGeography() { + this.geography = new Geography(this._matchRequest); + this.geography.init(); + } + _setExclusions() { this.exclusions = []; if (this._matchRequest.identifier) diff --git a/src/modules/matcher/tests/unit/geography.spec.ts b/src/modules/matcher/tests/unit/geography.spec.ts new file mode 100644 index 0000000..3a728e9 --- /dev/null +++ b/src/modules/matcher/tests/unit/geography.spec.ts @@ -0,0 +1,85 @@ +import { Geography } from '../../domain/entities/geography'; + +describe('Geography entity', () => { + it('should be defined', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }); + expect(geography).toBeDefined(); + }); + + describe('init', () => { + it('should initialize a geography request', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }); + geography.init(); + expect(geography.waypoints.length).toBe(2); + }); + it('should throw an exception if waypoints are empty', () => { + const geography = new Geography({ + waypoints: [], + }); + expect(() => geography.init()).toThrow(); + }); + it('should throw an exception if only one waypoint is provided', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + ], + }); + expect(() => geography.init()).toThrow(); + }); + it('should throw an exception if a waypoint has invalid longitude', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 201.093912, + }, + { + lat: 50.630992, + lon: 3.045432, + }, + ], + }); + expect(() => geography.init()).toThrow(); + }); + it('should throw an exception if a waypoint has invalid latitude', () => { + const geography = new Geography({ + waypoints: [ + { + lat: 49.440041, + lon: 1.093912, + }, + { + lat: 250.630992, + lon: 3.045432, + }, + ], + }); + expect(() => geography.init()).toThrow(); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/time.spec.ts b/src/modules/matcher/tests/unit/time.spec.ts index 4674045..0d8cbdd 100644 --- a/src/modules/matcher/tests/unit/time.spec.ts +++ b/src/modules/matcher/tests/unit/time.spec.ts @@ -123,6 +123,17 @@ describe('Time entity', () => { ); expect(() => time.init()).toThrow(); }); + it('should throw an exception if recurrent toDate is before fromDate', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2023-03-01', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); it('should throw an exception if schedule is missing', () => { const time = new Time( {