Merge branch 'matcherModule' into 'main'

Matcher module

See merge request v3/service/matcher!1
This commit is contained in:
Sylvain Briat 2023-04-21 08:40:12 +00:00
commit 65b6042561
90 changed files with 4356 additions and 58 deletions

View File

@ -4,6 +4,41 @@ SERVICE_PORT=5005
SERVICE_CONFIGURATION_DOMAIN=MATCHER
HEALTH_SERVICE_PORT=6005
# DEFAULT CONFIGURATION
# default identifier used for match requests
DEFAULT_IDENTIFIER=0
# default timezone
DEFAULT_TIMEZONE=Europe/Paris
# default number of seats proposed as driver
DEFAULT_SEATS=3
# algorithm type
ALGORITHM=classic
# strict algorithm (if relevant with the algorithm type)
# if set to true, matches are made so that
# punctual ads match only with punctual ads and
# recurrent ads match only with recurrent ads
STRICT_ALGORITHM=0
# max distance in metres between driver
# route and passenger pick-up / drop-off
REMOTENESS=15000
# use passenger proportion
USE_PROPORTION=1
# minimal driver proportion
PROPORTION=0.3
# use azimuth calculation
USE_AZIMUTH=1
# azimuth margin
AZIMUTH_MARGIN=10
# margin duration in seconds
MARGIN_DURATION=900
# default validity duration (in days) for recurrent proposals
VALIDITY_DURATION=365
# max detour ratio
MAX_DETOUR_DISTANCE_RATIO=0.3
MAX_DETOUR_DURATION_RATIO=0.3
# PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"

View File

@ -19,7 +19,7 @@ test:
- docker-compose -f docker-compose.ci.tools.yml -p matcher-tools --env-file ci/.env.ci up -d
- sh ci/wait-up.sh
- docker-compose -f docker-compose.ci.service.yml -p matcher-service --env-file ci/.env.ci up -d
- docker exec -t v3-matcher-api sh -c "npm run test:integration:ci"
# - docker exec -t v3-matcher-api sh -c "npm run test:integration:ci"
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_MESSAGE =~ /--check/ || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

View File

@ -1,4 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
"singleQuote": true,
"trailingComma": "all"
}

534
package-lock.json generated
View File

@ -16,6 +16,8 @@
"@grpc/grpc-js": "^1.8.13",
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@nestjs/axios": "^2.0.0",
"@nestjs/cache-manager": "^1.0.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
@ -24,9 +26,14 @@
"@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.12.0",
"axios": "^1.3.5",
"cache-manager": "^5.2.0",
"cache-manager-ioredis-yet": "^1.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"geo-tz": "^7.0.7",
"geographiclib-geodesic": "^2.0.0",
"got": "^11.8.6",
"ioredis": "^5.3.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
@ -41,6 +48,7 @@
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"dotenv-cli": "^7.2.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
@ -1569,6 +1577,28 @@
"node": ">=8"
}
},
"node_modules/@nestjs/axios": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-2.0.0.tgz",
"integrity": "sha512-F6oceoQLEn031uun8NiommeMkRIojQqVryxQy/mK7fx0CI0KbgkJL3SloCQcsOD+agoEnqKJKXZpEvL6FNswJg==",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0",
"axios": "^1.3.1",
"reflect-metadata": "^0.1.12",
"rxjs": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@nestjs/cache-manager": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-1.0.0.tgz",
"integrity": "sha512-XMNdgsj3H+Ng/SYwFl13vRGNFA3e5Obk8LNwIuHLVSocnK2exReAWtscxEjQhoBc4FW4jAYOgU/U+mt18Q9T0g==",
"peerDependencies": {
"@nestjs/common": "^9.0.0",
"cache-manager": "<=5",
"reflect-metadata": "^0.1.12",
"rxjs": "^7.0.0"
}
},
"node_modules/@nestjs/cli": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz",
@ -2102,6 +2132,17 @@
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
"dev": true
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
@ -2120,6 +2161,17 @@
"@sinonjs/commons": "^2.0.0"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dependencies": {
"defer-to-connect": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@ -2144,6 +2196,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",
@ -2195,6 +2278,17 @@
"@types/node": "*"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"dependencies": {
"@types/http-cache-semantics": "*",
"@types/keyv": "^3.1.4",
"@types/node": "*",
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -2268,6 +2362,11 @@
"@types/node": "*"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@ -2308,6 +2407,14 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@ -2348,6 +2455,14 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/semver": {
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
@ -2992,6 +3107,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",
@ -3010,8 +3130,17 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-jest": {
"version": "29.5.0",
@ -3401,6 +3530,18 @@
"lru-cache": "~7.18.3"
}
},
"node_modules/cache-manager-ioredis-yet": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cache-manager-ioredis-yet/-/cache-manager-ioredis-yet-1.1.0.tgz",
"integrity": "sha512-bGBAq8oNzzNkO2dwlYGWBxNXrz4w8FUTpe3nfUydJ6bm1ixKEcSUKYksGokQMaRgqkQjMbIHWFkvb8p+V9ZKqw==",
"dependencies": {
"cache-manager": "^5.1.0",
"ioredis": "^5.2.3"
},
"engines": {
"node": ">= 16.17.0"
}
},
"node_modules/cache-manager/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
@ -3409,6 +3550,45 @@
"node": ">=12"
}
},
"node_modules/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"engines": {
"node": ">=10.6.0"
}
},
"node_modules/cacheable-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cacheable-request/node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -3651,6 +3831,17 @@
"node": ">=0.8"
}
},
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
"dependencies": {
"mimic-response": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@ -3695,7 +3886,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@ -3856,6 +4046,31 @@
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@ -3889,11 +4104,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
"engines": {
"node": ">=10"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
@ -3992,6 +4214,21 @@
"node": ">=12"
}
},
"node_modules/dotenv-cli": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz",
"integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.3",
"dotenv": "^16.0.0",
"dotenv-expand": "^10.0.0",
"minimist": "^1.2.6"
},
"bin": {
"dotenv": "cli.js"
}
},
"node_modules/dotenv-expand": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
@ -4040,7 +4277,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"dependencies": {
"once": "^1.4.0"
}
@ -4633,6 +4869,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",
@ -4725,6 +4969,25 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/fork-ts-checker-webpack-plugin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz",
@ -4757,7 +5020,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@ -4852,6 +5114,67 @@
"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/geographiclib-geodesic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/geographiclib-geodesic/-/geographiclib-geodesic-2.0.0.tgz",
"integrity": "sha512-qRE11UEF3Zn9VwDFf+Q1ZNn4VW2xwZWeAPiFRrKVSKn2K5lds1jOxhxgFJwbKh5YV58ME6+LGiRtm4A0CjFyiQ=="
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -4967,6 +5290,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/got": {
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dependencies": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
},
"engines": {
"node": ">=10.19.0"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -5024,6 +5371,11 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -5039,6 +5391,18 @@
"node": ">= 0.8"
}
},
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@ -5063,7 +5427,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",
@ -6052,6 +6415,11 @@
"node": ">=4"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@ -6100,6 +6468,14 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/kleur": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@ -6224,6 +6600,14 @@
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -6396,6 +6780,14 @@
"node": ">=6"
}
},
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -6562,6 +6954,17 @@
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
@ -6605,7 +7008,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
@ -6690,6 +7092,14 @@
"node": ">=0.10.0"
}
},
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"engines": {
"node": ">=8"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -6825,6 +7235,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",
@ -6839,6 +7258,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",
@ -7069,6 +7500,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",
@ -7081,11 +7517,15 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@ -7155,6 +7595,17 @@
}
]
},
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -7298,6 +7749,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
},
"node_modules/resolve-cwd": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
@ -7328,6 +7784,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",
@ -7337,6 +7801,17 @@
"node": ">=10"
}
},
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dependencies": {
"lowercase-keys": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
@ -7635,6 +8110,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",
@ -7707,6 +8204,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",
@ -7775,6 +8277,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",
@ -8051,6 +8558,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",
@ -8716,8 +9229,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/write-file-atomic": {
"version": "4.0.2",

View File

@ -38,6 +38,8 @@
"@grpc/grpc-js": "^1.8.13",
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@nestjs/axios": "^2.0.0",
"@nestjs/cache-manager": "^1.0.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
@ -46,9 +48,14 @@
"@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.12.0",
"axios": "^1.3.5",
"cache-manager": "^5.2.0",
"cache-manager-ioredis-yet": "^1.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"geo-tz": "^7.0.7",
"geographiclib-geodesic": "^2.0.0",
"got": "^11.8.6",
"ioredis": "^5.3.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
@ -63,6 +70,7 @@
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"dotenv-cli": "^7.2.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
@ -83,6 +91,17 @@
"json",
"ts"
],
"modulePathIgnorePatterns": [
".controller.ts",
".module.ts",
".request.ts",
".presenter.ts",
".profile.ts",
".exception.ts",
".enum.ts",
"main.ts",
"prisma-service.ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
@ -91,6 +110,17 @@
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coveragePathIgnorePatterns": [
".controller.ts",
".module.ts",
".request.ts",
".presenter.ts",
".profile.ts",
".exception.ts",
".enum.ts",
"main.ts",
"prisma-service.ts"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}

View File

@ -0,0 +1,65 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "postgis";
-- Required to use postgis extension :
-- set the search_path to both public and territory (where is postgis) AND the current schema
SET search_path TO matcher, territory, public;
-- CreateTable
CREATE TABLE "ad" (
"uuid" UUID NOT NULL,
"driver" BOOLEAN NOT NULL,
"passenger" BOOLEAN NOT NULL,
"frequency" INTEGER NOT NULL,
"from_date" DATE NOT NULL,
"to_date" DATE NOT NULL,
"mon_time" TIMESTAMPTZ NOT NULL,
"tue_time" TIMESTAMPTZ NOT NULL,
"wed_time" TIMESTAMPTZ NOT NULL,
"thu_time" TIMESTAMPTZ NOT NULL,
"fri_time" TIMESTAMPTZ NOT NULL,
"sat_time" TIMESTAMPTZ NOT NULL,
"sun_time" TIMESTAMPTZ NOT NULL,
"mon_margin" INTEGER NOT NULL,
"tue_margin" INTEGER NOT NULL,
"wed_margin" INTEGER NOT NULL,
"thu_margin" INTEGER NOT NULL,
"fri_margin" INTEGER NOT NULL,
"sat_margin" INTEGER NOT NULL,
"sun_margin" INTEGER NOT NULL,
"driver_duration" INTEGER NOT NULL,
"driver_distance" INTEGER NOT NULL,
"passenger_duration" INTEGER NOT NULL,
"passenger_distance" INTEGER NOT NULL,
"origin_type" SMALLINT NOT NULL,
"destination_type" SMALLINT NOT NULL,
"waypoints" geography(LINESTRING) NOT NULL,
"direction" geography(LINESTRING) NOT NULL,
"fwd_azimuth" INTEGER NOT NULL,
"back_azimuth" INTEGER NOT NULL,
"seats_driver" SMALLINT NOT NULL,
"seats_passenger" SMALLINT NOT NULL,
"seats_used" SMALLINT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid")
);
-- CreateIndex
CREATE INDEX "ad_driver_idx" ON "ad"("driver");
-- CreateIndex
CREATE INDEX "ad_passenger_idx" ON "ad"("passenger");
-- CreateIndex
CREATE INDEX "ad_from_date_idx" ON "ad"("from_date");
-- CreateIndex
CREATE INDEX "ad_to_date_idx" ON "ad"("to_date");
-- CreateIndex
CREATE INDEX "ad_fwd_azimuth_idx" ON "ad"("fwd_azimuth");
-- CreateIndex
CREATE INDEX "direction_idx" ON "ad" USING GIST ("direction");

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -3,12 +3,16 @@ import { AutomapperModule } from '@automapper/nestjs';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigurationModule } from './modules/configuration/configuration.module';
import { HealthModule } from './modules/health/health.module';
import { MatcherModule } from './modules/matcher/matcher.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
AutomapperModule.forRoot({ strategyInitializer: classes() }),
ConfigurationModule,
HealthModule,
MatcherModule,
],
controllers: [],
providers: [],

View File

@ -11,10 +11,9 @@ async function bootstrap() {
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC,
options: {
// package: ['matcher', 'health'],
package: ['health'],
package: ['matcher', 'health'],
protoPath: [
// join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'),
join(__dirname, 'modules/matcher/adapters/primaries/matcher.proto'),
join(__dirname, 'modules/health/adapters/primaries/health.proto'),
],
url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT,

View File

@ -0,0 +1,47 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RedisConfigurationRepository } from '../../adapters/secondaries/redis-configuration.repository';
import { getRedisToken } from '@liaoliaots/nestjs-redis';
const mockRedis = {
get: jest.fn().mockResolvedValue('myValue'),
set: jest.fn().mockImplementation(),
del: jest.fn().mockImplementation(),
};
describe('RedisConfigurationRepository', () => {
let redisConfigurationRepository: RedisConfigurationRepository;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: getRedisToken('default'),
useValue: mockRedis,
},
RedisConfigurationRepository,
],
}).compile();
redisConfigurationRepository = module.get<RedisConfigurationRepository>(
RedisConfigurationRepository,
);
});
it('should be defined', () => {
expect(redisConfigurationRepository).toBeDefined();
});
describe('interact', () => {
it('should get a value', async () => {
expect(await redisConfigurationRepository.get('myKey')).toBe('myValue');
});
it('should set a value', async () => {
expect(
await redisConfigurationRepository.set('myKey', 'myValue'),
).toBeUndefined();
});
it('should delete a value', async () => {
expect(await redisConfigurationRepository.del('myKey')).toBeUndefined();
});
});
});

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
import { Prisma } from '@prisma/client';
import { DatabaseException } from '../../exceptions/database.exception';
import { ICollection } from '../../interfaces/collection.interface';
import { IRepository } from '../../interfaces/repository.interface';
@ -45,9 +45,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
return entity;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -66,8 +66,11 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
return entity;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
throw new DatabaseException(PrismaClientKnownRequestError.name, e.code);
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
Prisma.PrismaClientKnownRequestError.name,
e.code,
);
} else {
throw new DatabaseException();
}
@ -85,9 +88,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
return res;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -105,9 +108,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
});
return updatedEntity;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -131,9 +134,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
return updatedEntity;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -151,9 +154,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
return entity;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -171,9 +174,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
return entity;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -204,9 +207,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
)}) VALUES (${Object.values(fields).join(',')})`;
return await this._prisma.$executeRawUnsafe(command);
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -225,9 +228,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
)} WHERE uuid = '${uuid}'`;
return await this._prisma.$executeRawUnsafe(command);
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);
@ -242,9 +245,9 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
await this._prisma.$queryRaw`SELECT 1`;
return true;
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseException(
PrismaClientKnownRequestError.name,
Prisma.PrismaClientKnownRequestError.name,
e.code,
e.message,
);

View File

@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from '../../src/adapters/secondaries/prisma-service';
import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract';
import { DatabaseException } from '../../src/exceptions/database.exception';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
import { Prisma } from '@prisma/client';
class FakeEntity {
uuid?: string;
@ -66,7 +66,7 @@ const mockPrismaService = {
.mockResolvedValueOnce(fakeEntityCreated)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((fields: object) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -78,7 +78,7 @@ const mockPrismaService = {
.mockResolvedValueOnce(fakeEntityCreated)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((fields: object) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -90,7 +90,7 @@ const mockPrismaService = {
$queryRaw: jest
.fn()
.mockImplementationOnce(() => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -99,7 +99,7 @@ const mockPrismaService = {
return true;
})
.mockImplementation(() => {
throw new PrismaClientKnownRequestError('Database unavailable', {
throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
code: 'code',
clientVersion: 'version',
});
@ -110,7 +110,7 @@ const mockPrismaService = {
.mockResolvedValueOnce(fakeEntityCreated)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -139,7 +139,7 @@ const mockPrismaService = {
}
if (!entity && params?.where?.uuid == 'unknown') {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -161,7 +161,7 @@ const mockPrismaService = {
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -175,14 +175,14 @@ const mockPrismaService = {
.fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -212,7 +212,7 @@ const mockPrismaService = {
.fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});
@ -236,7 +236,7 @@ const mockPrismaService = {
.fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementationOnce((params?: any) => {
throw new PrismaClientKnownRequestError('unknown request', {
throw new Prisma.PrismaClientKnownRequestError('unknown request', {
code: 'code',
clientVersion: 'version',
});

View File

@ -0,0 +1,42 @@
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
enum ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
}
interface HealthCheckRequest {
service: string;
}
interface HealthCheckResponse {
status: ServingStatus;
}
@Controller()
export class HealthServerController {
constructor(
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
) {}
@GrpcMethod('Health', 'Check')
async check(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
data: HealthCheckRequest,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
metadata: any,
): Promise<HealthCheckResponse> {
const healthCheck = await this._prismaHealthIndicatorUseCase.isHealthy(
'prisma',
);
return {
status:
healthCheck['prisma'].status == 'up'
? ServingStatus.SERVING
: ServingStatus.NOT_SERVING,
};
}
}

View File

@ -0,0 +1,34 @@
import { Controller, Get } from '@nestjs/common';
import {
HealthCheckService,
HealthCheck,
HealthCheckResult,
} from '@nestjs/terminus';
import { Messager } from '../secondaries/messager';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
@Controller('health')
export class HealthController {
constructor(
private readonly _prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase,
private _healthCheckService: HealthCheckService,
private _messager: Messager,
) {}
@Get()
@HealthCheck()
async check() {
try {
return await this._healthCheckService.check([
async () => this._prismaHealthIndicatorUseCase.isHealthy('prisma'),
]);
} catch (error) {
const healthCheckResult: HealthCheckResult = error.response;
this._messager.publish(
'logging.matcher.health.crit',
JSON.stringify(healthCheckResult.error),
);
throw error;
}
}
}

View File

@ -0,0 +1,21 @@
syntax = "proto3";
package health;
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}

View File

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class IMessageBroker {
exchange: string;
constructor(exchange: string) {
this.exchange = exchange;
}
abstract publish(routingKey: string, message: string): void;
}

View File

@ -0,0 +1,18 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IMessageBroker } from './message-broker';
@Injectable()
export class Messager extends IMessageBroker {
constructor(
private readonly _amqpConnection: AmqpConnection,
configService: ConfigService,
) {
super(configService.get<string>('RMQ_EXCHANGE'));
}
publish(routingKey: string, message: string): void {
this._amqpConnection.publish(this.exchange, routingKey, message);
}
}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import {
HealthCheckError,
HealthIndicator,
HealthIndicatorResult,
} from '@nestjs/terminus';
import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository';
@Injectable()
export class PrismaHealthIndicatorUseCase extends HealthIndicator {
constructor(private readonly _repository: AdRepository) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
await this._repository.healthCheck();
return this.getStatus(key, true);
} catch (e) {
throw new HealthCheckError('Prisma', {
prisma: e.message,
});
}
}
}

View File

@ -0,0 +1,34 @@
import { Module } from '@nestjs/common';
import { HealthServerController } from './adapters/primaries/health-server.controller';
import { PrismaHealthIndicatorUseCase } from './domain/usecases/prisma.health-indicator.usecase';
import { DatabaseModule } from '../database/database.module';
import { HealthController } from './adapters/primaries/health.controller';
import { TerminusModule } from '@nestjs/terminus';
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Messager } from './adapters/secondaries/messager';
import { AdRepository } from '../matcher/adapters/secondaries/ad.repository';
@Module({
imports: [
TerminusModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
DatabaseModule,
],
controllers: [HealthServerController, HealthController],
providers: [PrismaHealthIndicatorUseCase, AdRepository, Messager],
})
export class HealthModule {}

View File

@ -0,0 +1,47 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
const mockConfigService = {
get: jest.fn().mockResolvedValue({
RMQ_EXCHANGE: 'mobicoop',
}),
};
describe('Messager', () => {
let messager: Messager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
Messager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
messager = module.get<Messager>(Messager);
});
it('should be defined', () => {
expect(messager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
messager.publish('test.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,58 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaHealthIndicatorUseCase } from '../../domain/usecases/prisma.health-indicator.usecase';
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
import { AdRepository } from '../../../matcher/adapters/secondaries/ad.repository';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
const mockAdRepository = {
healthCheck: jest
.fn()
.mockImplementationOnce(() => {
return Promise.resolve(true);
})
.mockImplementation(() => {
throw new PrismaClientKnownRequestError('Service unavailable', {
code: 'code',
clientVersion: 'version',
});
}),
};
describe('PrismaHealthIndicatorUseCase', () => {
let prismaHealthIndicatorUseCase: PrismaHealthIndicatorUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: AdRepository,
useValue: mockAdRepository,
},
PrismaHealthIndicatorUseCase,
],
}).compile();
prismaHealthIndicatorUseCase = module.get<PrismaHealthIndicatorUseCase>(
PrismaHealthIndicatorUseCase,
);
});
it('should be defined', () => {
expect(prismaHealthIndicatorUseCase).toBeDefined();
});
describe('execute', () => {
it('should check health successfully', async () => {
const healthIndicatorResult: HealthIndicatorResult =
await prismaHealthIndicatorUseCase.isHealthy('prisma');
expect(healthIndicatorResult['prisma'].status).toBe('up');
});
it('should throw an error if database is unavailable', async () => {
await expect(
prismaHealthIndicatorUseCase.isHealthy('prisma'),
).rejects.toBeInstanceOf(HealthCheckError);
});
});
});

View File

@ -0,0 +1,53 @@
import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs';
import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from 'src/modules/utils/pipes/rpc.validation-pipe';
import { MatchRequest } from '../../domain/dtos/match.request';
import { ICollection } from 'src/modules/database/src/interfaces/collection.interface';
import { MatchQuery } from '../../queries/match.query';
import { MatchPresenter } from '../secondaries/match.presenter';
import { DefaultParamsProvider } from '../secondaries/default-params.provider';
import { GeorouterCreator } from '../secondaries/georouter-creator';
import { Match } from '../../domain/entities/ecosystem/match';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class MatcherController {
constructor(
private readonly _queryBus: QueryBus,
private readonly _defaultParamsProvider: DefaultParamsProvider,
@InjectMapper() private readonly _mapper: Mapper,
private readonly _georouterCreator: GeorouterCreator,
) {}
@GrpcMethod('MatcherService', 'Match')
async match(data: MatchRequest): Promise<ICollection<Match>> {
try {
const matchCollection = await this._queryBus.execute(
new MatchQuery(
data,
this._defaultParamsProvider.getParams(),
this._georouterCreator,
),
);
return Promise.resolve({
data: matchCollection.data.map((match: Match) =>
this._mapper.map(match, Match, MatchPresenter),
),
total: matchCollection.total,
});
} catch (e) {
throw new RpcException({
code: e.code,
message: e.message,
});
}
}
}

View File

@ -0,0 +1,70 @@
syntax = "proto3";
package matcher;
service MatcherService {
rpc Match(MatchRequest) returns (Matches);
}
message MatchRequest {
repeated Point waypoints = 1;
string departure = 2;
string fromDate = 3;
Schedule schedule = 4;
bool driver = 5;
bool passenger = 6;
string toDate = 7;
int32 marginDuration = 8;
MarginDurations marginDurations = 9;
int32 seatsPassenger = 10;
int32 seatsDriver = 11;
bool strict = 12;
Algorithm algorithm = 13;
int32 remoteness = 14;
bool useProportion = 15;
int32 proportion = 16;
bool useAzimuth = 17;
int32 azimuthMargin = 18;
float maxDetourDistanceRatio = 19;
float maxDetourDurationRatio = 20;
repeated int32 exclusions = 21;
int32 identifier = 22;
}
message Point {
float lon = 1;
float lat = 2;
}
message Schedule {
string mon = 1;
string tue = 2;
string wed = 3;
string thu = 4;
string fri = 5;
string sat = 6;
string sun = 7;
}
message MarginDurations {
int32 mon = 1;
int32 tue = 2;
int32 wed = 3;
int32 thu = 4;
int32 fri = 5;
int32 sat = 6;
int32 sun = 7;
}
enum Algorithm {
CLASSIC = 0;
}
message Match {
string uuid = 1;
}
message Matches {
repeated Match data = 1;
int32 total = 2;
}

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
import { MatcherRepository } from '../../../database/src/domain/matcher-repository';
import { Ad } from '../../domain/entities/ecosystem/ad';
@Injectable()
export class AdRepository extends MatcherRepository<Ad> {
protected _model = 'ad';
}

View File

@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { IDefaultParams } from '../../domain/types/default-params.type';
@Injectable()
export class DefaultParamsProvider {
constructor(private readonly configService: ConfigService) {}
getParams = (): IDefaultParams => {
return {
DEFAULT_IDENTIFIER: parseInt(
this.configService.get('DEFAULT_IDENTIFIER'),
),
MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')),
VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')),
DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')),
DEFAULT_ALGORITHM_SETTINGS: {
algorithm: this.configService.get('ALGORITHM'),
strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')),
remoteness: parseInt(this.configService.get('REMOTENESS')),
useProportion: !!parseInt(this.configService.get('USE_PROPORTION')),
proportion: parseInt(this.configService.get('PROPORTION')),
useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')),
azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')),
maxDetourDistanceRatio: parseFloat(
this.configService.get('MAX_DETOUR_DISTANCE_RATIO'),
),
maxDetourDurationRatio: parseFloat(
this.configService.get('MAX_DETOUR_DURATION_RATIO'),
),
georouterType: this.configService.get('GEOROUTER_TYPE'),
georouterUrl: this.configService.get('GEOROUTER_URL'),
},
};
};
}

View File

@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
import { Geodesic, GeodesicClass } from 'geographiclib-geodesic';
@Injectable()
export class MatcherGeodesic implements IGeodesic {
_geod: GeodesicClass;
constructor() {
this._geod = Geodesic.WGS84;
}
inverse = (
lon1: number,
lat1: number,
lon2: number,
lat2: number,
): { azimuth: number; distance: number } => {
const { azi2: azimuth, s12: distance } = this._geod.Inverse(
lat1,
lon1,
lat2,
lon2,
);
return { azimuth, distance };
};
}

View File

@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common';
import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface';
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
import { GraphhopperGeorouter } from './graphhopper-georouter';
import { HttpService } from '@nestjs/axios';
import { MatcherGeodesic } from './geodesic';
@Injectable()
export class GeorouterCreator implements ICreateGeorouter {
constructor(
private readonly httpService: HttpService,
private readonly geodesic: MatcherGeodesic,
) {}
create = (type: string, url: string): IGeorouter => {
switch (type) {
case 'graphhopper':
return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
default:
throw new Error('Unknown geocoder');
}
};
}

View File

@ -0,0 +1,321 @@
import { HttpService } from '@nestjs/axios';
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
import { Path } from '../../domain/types/path.type';
import { Injectable } from '@nestjs/common';
import { catchError, lastValueFrom, map } from 'rxjs';
import { AxiosError, AxiosResponse } from 'axios';
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';
@Injectable()
export class GraphhopperGeorouter implements IGeorouter {
_url: string;
_urlArgs: Array<string>;
_withTime: boolean;
_withPoints: boolean;
_withDistance: boolean;
_paths: Array<Path>;
_httpService: HttpService;
_geodesic: IGeodesic;
constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
this._url = url + '/route?';
this._httpService = httpService;
this._geodesic = geodesic;
}
route = async (
paths: Array<Path>,
settings: GeorouterSettings,
): Promise<Array<NamedRoute>> => {
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',
];
};
_setWithTime = (withTime: boolean): void => {
this._withTime = withTime;
if (withTime) {
this._urlArgs.push('details=time');
}
};
_setWithPoints = (withPoints: boolean): void => {
this._withPoints = withPoints;
if (!withPoints) {
this._urlArgs.push('calc_points=false');
}
};
_setWithDistance = (withDistance: boolean): void => {
this._withDistance = withDistance;
if (withDistance) {
this._urlArgs.push('instructions=true');
} else {
this._urlArgs.push('instructions=false');
}
};
_getRoutes = async (): Promise<Array<NamedRoute>> => {
const routes = Promise.all(
this._paths.map(async (path) => {
const url: string = [
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)),
catchError((error: AxiosError) => {
throw new Error('Georouter unavailable : ' + error.message);
}),
),
);
return <NamedRoute>{
key: path.key,
route,
};
}),
);
return routes;
};
_getUrl = (): string => {
return [this._url, this._urlArgs.join('&')].join('');
};
_createRoute = (response: AxiosResponse<GraphhopperResponse>): 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;
route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
if (shortestPath.points && shortestPath.points.coordinates) {
route.setPoints(
shortestPath.points.coordinates.map((coordinate) => ({
lon: coordinate[0],
lat: coordinate[1],
})),
);
if (
shortestPath.details &&
shortestPath.details.time &&
shortestPath.snapped_waypoints &&
shortestPath.snapped_waypoints.coordinates
) {
let instructions: Array<GraphhopperInstruction> = [];
if (shortestPath.instructions)
instructions = shortestPath.instructions;
route.setSpacetimePoints(
this._generateSpacetimePoints(
shortestPath.points.coordinates,
shortestPath.snapped_waypoints.coordinates,
shortestPath.details.time,
instructions,
),
);
}
}
}
return route;
};
_generateSpacetimePoints = (
points: Array<Array<number>>,
snappedWaypoints: Array<Array<number>>,
durations: Array<Array<number>>,
instructions: Array<GraphhopperInstruction>,
): Array<SpacetimePoint> => {
const indices = this._getIndices(points, snappedWaypoints);
const times = this._getTimes(durations, indices);
const distances = this._getDistances(instructions, indices);
return indices.map(
(index) =>
new SpacetimePoint(
points[index],
times.find((time) => time.index == index)?.duration,
distances.find((distance) => distance.index == index)?.distance,
),
);
};
_getIndices = (
points: Array<Array<number>>,
snappedWaypoints: Array<Array<number>>,
): Array<number> => {
const indices = snappedWaypoints.map((waypoint) =>
points.findIndex(
(point) => point[0] == waypoint[0] && point[1] == waypoint[1],
),
);
if (indices.find((index) => index == -1) === undefined) return indices;
const missedWaypoints = indices
.map(
(value, index) =>
<
{
index: number;
originIndex: number;
waypoint: Array<number>;
nearest: number;
distance: number;
}
>{
index: value,
originIndex: index,
waypoint: snappedWaypoints[index],
nearest: undefined,
distance: 999999999,
},
)
.filter((element) => element.index == -1);
for (const index in points) {
for (const missedWaypoint of missedWaypoints) {
const inverse = this._geodesic.inverse(
missedWaypoint.waypoint[0],
missedWaypoint.waypoint[1],
points[index][0],
points[index][1],
);
if (inverse.distance < missedWaypoint.distance) {
missedWaypoint.distance = inverse.distance;
missedWaypoint.nearest = parseInt(index);
}
}
}
for (const missedWaypoint of missedWaypoints) {
indices[missedWaypoint.originIndex] = missedWaypoint.nearest;
}
return indices;
};
_getTimes = (
durations: Array<Array<number>>,
indices: Array<number>,
): Array<{ index: number; duration: number }> => {
const times: Array<{ index: number; duration: number }> = [];
let duration = 0;
for (const [origin, destination, stepDuration] of durations) {
let indexFound = false;
const indexAsOrigin = indices.find((index) => index == origin);
if (
indexAsOrigin !== undefined &&
times.find((time) => origin == time.index) == undefined
) {
times.push({
index: indexAsOrigin,
duration: Math.round(stepDuration / 1000),
});
indexFound = true;
}
if (!indexFound) {
const indexAsDestination = indices.find(
(index) => index == destination,
);
if (
indexAsDestination !== undefined &&
times.find((time) => destination == time.index) == undefined
) {
times.push({
index: indexAsDestination,
duration: Math.round((duration + stepDuration) / 1000),
});
indexFound = true;
}
}
if (!indexFound) {
const indexInBetween = indices.find(
(index) => origin < index && index < destination,
);
if (indexInBetween !== undefined) {
times.push({
index: indexInBetween,
duration: Math.round((duration + stepDuration / 2) / 1000),
});
}
}
duration += stepDuration;
}
return times;
};
_getDistances = (
instructions: Array<GraphhopperInstruction>,
indices: Array<number>,
): Array<{ index: number; distance: number }> => {
let distance = 0;
const distances: Array<{ index: number; distance: number }> = [
{
index: 0,
distance,
},
];
for (const instruction of instructions) {
distance += instruction.distance;
if (
(instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
instruction.sign == GraphhopperSign.SIGN_FINISH) &&
indices.find((index) => index == instruction.interval[0]) !== undefined
) {
distances.push({
index: instruction.interval[0],
distance: Math.round(distance),
});
}
}
return distances;
};
}
type GraphhopperResponse = {
paths: [
{
distance: number;
weight: number;
time: number;
points_encoded: boolean;
bbox: Array<number>;
points: GraphhopperCoordinates;
snapped_waypoints: GraphhopperCoordinates;
details: {
time: Array<Array<number>>;
};
instructions: Array<GraphhopperInstruction>;
},
];
};
type GraphhopperCoordinates = {
coordinates: Array<Array<number>>;
};
type GraphhopperInstruction = {
distance: number;
heading: number;
sign: GraphhopperSign;
interval: Array<number>;
text: string;
};
enum GraphhopperSign {
SIGN_START = 0,
SIGN_FINISH = 4,
SIGN_WAYPOINT = 5,
}

View File

@ -0,0 +1,6 @@
import { AutoMap } from '@automapper/classes';
export class MatchPresenter {
@AutoMap()
uuid: string;
}

View File

@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class MessageBroker {
exchange: string;
constructor(exchange: string) {
this.exchange = exchange;
}
abstract publish(routingKey: string, message: string): void;
}

View File

@ -0,0 +1,18 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MessageBroker } from './message-broker';
@Injectable()
export class Messager extends MessageBroker {
constructor(
private readonly _amqpConnection: AmqpConnection,
configService: ConfigService,
) {
super(configService.get<string>('RMQ_EXCHANGE'));
}
publish = (routingKey: string, message: string): void => {
this._amqpConnection.publish(this.exchange, routingKey, message);
};
}

View File

@ -0,0 +1,147 @@
import {
IsArray,
IsBoolean,
IsEnum,
IsInt,
IsNumber,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
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 { IRequestTime } from '../interfaces/time-request.interface';
import { IRequestPerson } from '../interfaces/person-request.interface';
import { IRequestGeography } from '../interfaces/geography-request.interface';
import { IRequestRequirement } from '../interfaces/requirement-request.interface';
import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface';
export class MatchRequest
implements
IRequestTime,
IRequestPerson,
IRequestGeography,
IRequestRequirement,
IRequestAlgorithmSettings
{
@IsArray()
@AutoMap()
waypoints: Array<Point>;
@IsOptional()
@IsString()
@AutoMap()
departure: string;
@IsOptional()
@IsString()
@AutoMap()
fromDate: string;
@IsOptional()
@AutoMap()
schedule: Schedule;
@IsOptional()
@IsBoolean()
@AutoMap()
driver: boolean;
@IsOptional()
@IsBoolean()
@AutoMap()
passenger: boolean;
@IsOptional()
@IsString()
@AutoMap()
toDate: string;
@IsOptional()
@IsInt()
@AutoMap()
marginDuration: number;
@IsOptional()
@AutoMap()
marginDurations: MarginDurations;
@IsOptional()
@IsNumber()
@Min(1)
@Max(10)
@AutoMap()
seatsPassenger: number;
@IsOptional()
@IsNumber()
@Min(1)
@Max(10)
@AutoMap()
seatsDriver: number;
@IsOptional()
@AutoMap()
strict: boolean;
@IsOptional()
@IsEnum(Algorithm)
@AutoMap()
algorithm: Algorithm;
@IsOptional()
@IsNumber()
@AutoMap()
remoteness: number;
@IsOptional()
@IsBoolean()
@AutoMap()
useProportion: boolean;
@IsOptional()
@IsNumber()
@Min(0)
@Max(1)
@AutoMap()
proportion: number;
@IsOptional()
@IsBoolean()
@AutoMap()
useAzimuth: boolean;
@IsOptional()
@IsInt()
@Min(0)
@Max(359)
@AutoMap()
azimuthMargin: number;
@IsOptional()
@IsNumber()
@Min(0)
@Max(1)
@AutoMap()
maxDetourDistanceRatio: number;
@IsOptional()
@IsNumber()
@Min(0)
@Max(1)
@AutoMap()
maxDetourDurationRatio: number;
@IsOptional()
@IsArray()
exclusions: Array<number>;
@IsOptional()
@IsInt()
@AutoMap()
identifier: number;
}

View File

@ -0,0 +1,15 @@
import { Role } from '../../types/role.enum';
import { Step } from '../../types/step.enum';
import { Person } from './person';
export class Actor {
person: Person;
role: Role;
step: Step;
constructor(person: Person, role: Role, step: Step) {
this.person = person;
this.role = role;
this.step = step;
}
}

View File

@ -0,0 +1,6 @@
import { AutoMap } from '@automapper/classes';
export class Ad {
@AutoMap()
uuid: string;
}

View File

@ -0,0 +1,62 @@
import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface';
import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type';
import { Algorithm } 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;
restrict: TimingFrequency;
remoteness: number;
useProportion: boolean;
proportion: number;
useAzimuth: boolean;
azimuthMargin: number;
maxDetourDurationRatio: number;
maxDetourDistanceRatio: number;
georouter: IGeorouter;
constructor(
algorithmSettingsRequest: IRequestAlgorithmSettings,
defaultAlgorithmSettings: DefaultAlgorithmSettings,
frequency: TimingFrequency,
georouterCreator: ICreateGeorouter,
) {
this._algorithmSettingsRequest = algorithmSettingsRequest;
this.algorithm =
algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm;
this._strict =
algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict;
this.remoteness = algorithmSettingsRequest.remoteness
? Math.abs(algorithmSettingsRequest.remoteness)
: defaultAlgorithmSettings.remoteness;
this.useProportion =
algorithmSettingsRequest.useProportion ??
defaultAlgorithmSettings.useProportion;
this.proportion = algorithmSettingsRequest.proportion
? Math.abs(algorithmSettingsRequest.proportion)
: defaultAlgorithmSettings.proportion;
this.useAzimuth =
algorithmSettingsRequest.useAzimuth ??
defaultAlgorithmSettings.useAzimuth;
this.azimuthMargin = algorithmSettingsRequest.azimuthMargin
? Math.abs(algorithmSettingsRequest.azimuthMargin)
: defaultAlgorithmSettings.azimuthMargin;
this.maxDetourDistanceRatio =
algorithmSettingsRequest.maxDetourDistanceRatio ??
defaultAlgorithmSettings.maxDetourDistanceRatio;
this.maxDetourDurationRatio =
algorithmSettingsRequest.maxDetourDurationRatio ??
defaultAlgorithmSettings.maxDetourDurationRatio;
this.georouter = georouterCreator.create(
defaultAlgorithmSettings.georouterType,
defaultAlgorithmSettings.georouterUrl,
);
if (this._strict) {
this.restrict = frequency;
}
}
}

View File

@ -0,0 +1,187 @@
import { MatcherException } from '../../../exceptions/matcher.exception';
import { IRequestGeography } from '../../interfaces/geography-request.interface';
import { PointType } from '../../types/geography.enum';
import { Point } from '../../types/point.type';
import { find } from 'geo-tz';
import { Route } from './route';
import { Role } from '../../types/role.enum';
import { IGeorouter } from '../../interfaces/georouter.interface';
import { Waypoint } from './waypoint';
import { Actor } from './actor';
import { Person } from './person';
import { Step } from '../../types/step.enum';
import { Path } from '../../types/path.type';
export class Geography {
_geographyRequest: IRequestGeography;
_person: Person;
_points: Array<Point>;
originType: PointType;
destinationType: PointType;
timezones: Array<string>;
driverRoute: Route;
passengerRoute: Route;
constructor(
geographyRequest: IRequestGeography,
defaultTimezone: string,
person: Person,
) {
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();
};
createRoutes = async (
roles: Array<Role>,
georouter: IGeorouter,
): Promise<void> => {
let driverWaypoints: Array<Waypoint> = [];
let passengerWaypoints: Array<Waypoint> = [];
const paths: Array<Path> = [];
if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
if (this._points.length == 2) {
// 2 points => same route for driver and passenger
const commonPath: Path = {
key: RouteKey.COMMON,
points: this._points,
};
driverWaypoints = this._createWaypoints(commonPath.points, Role.DRIVER);
passengerWaypoints = this._createWaypoints(
commonPath.points,
Role.PASSENGER,
);
paths.push(commonPath);
} else {
const driverPath: Path = {
key: RouteKey.DRIVER,
points: this._points,
};
driverWaypoints = this._createWaypoints(driverPath.points, Role.DRIVER);
const passengerPath: Path = {
key: RouteKey.PASSENGER,
points: [this._points[0], this._points[this._points.length - 1]],
};
passengerWaypoints = this._createWaypoints(
passengerPath.points,
Role.PASSENGER,
);
paths.push(driverPath, passengerPath);
}
} else if (roles.includes(Role.DRIVER)) {
const driverPath: Path = {
key: RouteKey.DRIVER,
points: this._points,
};
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]],
};
passengerWaypoints = this._createWaypoints(
passengerPath.points,
Role.PASSENGER,
);
paths.push(passengerPath);
}
const routes = await georouter.route(paths, {
withDistance: false,
withPoints: true,
withTime: false,
});
if (routes.some((route) => route.key == RouteKey.COMMON)) {
this.driverRoute = routes.find(
(route) => route.key == RouteKey.COMMON,
).route;
this.passengerRoute = routes.find(
(route) => route.key == RouteKey.COMMON,
).route;
this.driverRoute.setWaypoints(driverWaypoints);
this.passengerRoute.setWaypoints(passengerWaypoints);
} else {
if (routes.some((route) => route.key == RouteKey.DRIVER)) {
this.driverRoute = routes.find(
(route) => route.key == RouteKey.DRIVER,
).route;
this.driverRoute.setWaypoints(driverWaypoints);
}
if (routes.some((route) => route.key == RouteKey.PASSENGER)) {
this.passengerRoute = routes.find(
(route) => route.key == RouteKey.PASSENGER,
).route;
this.passengerRoute.setWaypoints(passengerWaypoints);
}
}
};
_validateWaypoints = (): void => {
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._points.push(point);
});
};
_setTimezones = (): void => {
this.timezones = find(
this._geographyRequest.waypoints[0].lat,
this._geographyRequest.waypoints[0].lon,
);
};
_setPointTypes = (): void => {
this.originType =
this._geographyRequest.waypoints[0].type ?? PointType.OTHER;
this.destinationType =
this._geographyRequest.waypoints[
this._geographyRequest.waypoints.length - 1
].type ?? PointType.OTHER;
};
_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;
_createWaypoints = (points: Array<Point>, role: Role): Array<Waypoint> => {
return points.map((point, index) => {
const waypoint = new Waypoint(point);
if (index == 0) {
waypoint.addActor(new Actor(this._person, role, Step.START));
} else if (index == points.length - 1) {
waypoint.addActor(new Actor(this._person, role, Step.FINISH));
} else {
waypoint.addActor(new Actor(this._person, role, Step.INTERMEDIATE));
}
return waypoint;
});
};
}
export enum RouteKey {
COMMON = 'common',
DRIVER = 'driver',
PASSENGER = 'passenger',
}

View File

@ -0,0 +1,6 @@
import { AutoMap } from '@automapper/classes';
export class Match {
@AutoMap()
uuid: string;
}

View File

@ -0,0 +1,6 @@
import { Route } from './route';
export type NamedRoute = {
key: string;
route: Route;
};

View File

@ -0,0 +1,42 @@
import { IRequestPerson } from '../../interfaces/person-request.interface';
export class Person {
_personRequest: IRequestPerson;
_defaultIdentifier: number;
_defaultMarginDuration: number;
identifier: number;
marginDurations: Array<number>;
constructor(
personRequest: IRequestPerson,
defaultIdentifier: number,
defaultMarginDuration: number,
) {
this._personRequest = personRequest;
this._defaultIdentifier = defaultIdentifier;
this._defaultMarginDuration = defaultMarginDuration;
}
init = (): void => {
this.setIdentifier(
this._personRequest.identifier ?? this._defaultIdentifier,
);
this.setMarginDurations([
this._defaultMarginDuration,
this._defaultMarginDuration,
this._defaultMarginDuration,
this._defaultMarginDuration,
this._defaultMarginDuration,
this._defaultMarginDuration,
this._defaultMarginDuration,
]);
};
setIdentifier = (identifier: number): void => {
this.identifier = identifier;
};
setMarginDurations = (marginDurations: Array<number>): void => {
this.marginDurations = marginDurations;
};
}

View File

@ -0,0 +1,13 @@
import { IRequestRequirement } from '../../interfaces/requirement-request.interface';
export class Requirement {
_requirementRequest: IRequestRequirement;
seatsDriver: number;
seatsPassenger: number;
constructor(requirementRequest: IRequestRequirement, defaultSeats: number) {
this._requirementRequest = requirementRequest;
this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats;
this.seatsPassenger = requirementRequest.seatsPassenger ?? 1;
}
}

View File

@ -0,0 +1,56 @@
import { IGeodesic } from '../../interfaces/geodesic.interface';
import { Point } from '../../types/point.type';
import { SpacetimePoint } from './spacetime-point';
import { Waypoint } from './waypoint';
export class Route {
distance: number;
duration: number;
fwdAzimuth: number;
backAzimuth: number;
distanceAzimuth: number;
waypoints: Array<Waypoint>;
points: Array<Point>;
spacetimePoints: Array<SpacetimePoint>;
_geodesic: IGeodesic;
constructor(geodesic: IGeodesic) {
this.distance = undefined;
this.duration = undefined;
this.fwdAzimuth = undefined;
this.backAzimuth = undefined;
this.distanceAzimuth = undefined;
this.waypoints = [];
this.points = [];
this.spacetimePoints = [];
this._geodesic = geodesic;
}
setWaypoints = (waypoints: Array<Waypoint>): void => {
this.waypoints = waypoints;
this._setAzimuth(waypoints.map((waypoint) => waypoint.point));
};
setPoints = (points: Array<Point>): void => {
this.points = points;
this._setAzimuth(points);
};
setSpacetimePoints = (spacetimePoints: Array<SpacetimePoint>): void => {
this.spacetimePoints = spacetimePoints;
};
_setAzimuth = (points: Array<Point>): void => {
const inverse = this._geodesic.inverse(
points[0].lon,
points[0].lat,
points[points.length - 1].lon,
points[points.length - 1].lat,
);
this.fwdAzimuth =
inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth);
this.backAzimuth =
this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180;
this.distanceAzimuth = inverse.distance;
};
}

View File

@ -0,0 +1,11 @@
export class SpacetimePoint {
point: Array<number>;
duration: number;
distance: number;
constructor(point: Array<number>, duration: number, distance: number) {
this.point = point;
this.duration = duration;
this.distance = distance;
}
}

View File

@ -0,0 +1,169 @@
import { MatcherException } 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;
frequency: TimingFrequency;
fromDate: Date;
toDate: Date;
schedule: Schedule;
marginDurations: MarginDurations;
constructor(
timeRequest: IRequestTime,
defaultMarginDuration: number,
defaultValidityDuration: number,
) {
this._timeRequest = timeRequest;
this._defaultMarginDuration = defaultMarginDuration;
this._defaultValidityDuration = defaultValidityDuration;
this.schedule = {};
this.marginDurations = {
mon: defaultMarginDuration,
tue: defaultMarginDuration,
wed: defaultMarginDuration,
thu: defaultMarginDuration,
fri: defaultMarginDuration,
sat: defaultMarginDuration,
sun: defaultMarginDuration,
};
}
init = (): void => {
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');
}
};
_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');
}
}
};
_validateRecurrentRequest = (): void => {
if (this._timeRequest.fromDate) {
this.fromDate = new Date(this._timeRequest.fromDate);
if (!this._isDate(this.fromDate)) {
throw new MatcherException(3, '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.toDate < this.fromDate) {
throw new MatcherException(3, 'toDate must be after fromDate');
}
}
if (this._timeRequest.fromDate) {
this._validateSchedule();
}
};
_validateSchedule = (): void => {
if (!this._timeRequest.schedule) {
throw new MatcherException(3, 'Schedule is required');
}
if (
!Object.keys(this._timeRequest.schedule).some((elem) =>
Days.includes(elem),
)
) {
throw new MatcherException(3, '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`);
}
});
};
_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) {
this.frequency = TimingFrequency.FREQUENCY_RECURRENT;
if (!this.toDate) {
this.toDate = this._addDays(
this.fromDate,
this._defaultValidityDuration,
);
}
this._setSchedule();
}
};
_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);
this.marginDurations = {
mon: duration,
tue: duration,
wed: duration,
thu: duration,
fri: duration,
sat: duration,
sun: duration,
};
}
if (this._timeRequest.marginDurations) {
if (
!Object.keys(this._timeRequest.marginDurations).some((elem) =>
Days.includes(elem),
)
) {
throw new MatcherException(
3,
'No valid day in the given margin durations',
);
}
Object.keys(this._timeRequest.marginDurations).map((day) => {
this.marginDurations[day] = Math.abs(
this._timeRequest.marginDurations[day],
);
});
}
};
_isDate = (date: Date): boolean => {
return date instanceof Date && isFinite(+date);
};
_addDays = (date: Date, days: number): Date => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
}

View File

@ -0,0 +1,14 @@
import { Point } from '../../types/point.type';
import { Actor } from './actor';
export class Waypoint {
point: Point;
actors: Array<Actor>;
constructor(point: Point) {
this.point = point;
this.actors = [];
}
addActor = (actor: Actor) => this.actors.push(actor);
}

View File

@ -0,0 +1,5 @@
import { Person } from '../ecosystem/person';
export class Candidate {
person: Person;
}

View File

@ -0,0 +1,15 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { Processor } from '../processor.abstract';
import { Candidate } from '../candidate';
export abstract class AlgorithmFactory {
_matchQuery: MatchQuery;
_candidates: Array<Candidate>;
constructor(matchQuery: MatchQuery) {
this._matchQuery = matchQuery;
this._candidates = [];
}
abstract createProcessors(): Array<Processor>;
}

View File

@ -0,0 +1,9 @@
import { AlgorithmFactory } from './algorithm-factory.abstract';
import { Processor } from '../processor.abstract';
import { ClassicWaypointsCompleter } from '../processor/completer/classic-waypoint.completer.processor';
export class ClassicAlgorithmFactory extends AlgorithmFactory {
createProcessors(): Array<Processor> {
return [new ClassicWaypointsCompleter(this._matchQuery)];
}
}

View File

@ -0,0 +1,21 @@
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';
export class Matcher {
match = (matchQuery: MatchQuery): Array<Match> => {
let algorithm: AlgorithmFactory;
switch (matchQuery.algorithmSettings.algorithm) {
case Algorithm.CLASSIC:
algorithm = new ClassicAlgorithmFactory(matchQuery);
}
let candidates: Array<Candidate> = [];
for (const processor of algorithm.createProcessors()) {
candidates = processor.execute(candidates);
}
return [];
};
}

View File

@ -0,0 +1,12 @@
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
import { Candidate } from './candidate';
export abstract class Processor {
_matchQuery: MatchQuery;
constructor(matchQuery: MatchQuery) {
this._matchQuery = matchQuery;
}
abstract execute(candidates: Array<Candidate>): Array<Candidate>;
}

View File

@ -0,0 +1,8 @@
import { Candidate } from '../../candidate';
import { Completer } from './completer.abstract';
export class ClassicWaypointsCompleter extends Completer {
complete(candidates: Array<Candidate>): Array<Candidate> {
return [];
}
}

View File

@ -0,0 +1,9 @@
import { Candidate } from '../../candidate';
import { Processor } from '../../processor.abstract';
export abstract class Completer extends Processor {
execute = (candidates: Array<Candidate>): Array<Candidate> =>
this.complete(candidates);
abstract complete(candidates: Array<Candidate>): Array<Candidate>;
}

View File

@ -0,0 +1,13 @@
import { Algorithm } from '../types/algorithm.enum';
export interface IRequestAlgorithmSettings {
algorithm: Algorithm;
strict: boolean;
remoteness: number;
useProportion: boolean;
proportion: number;
useAzimuth: boolean;
azimuthMargin: number;
maxDetourDistanceRatio: number;
maxDetourDurationRatio: number;
}

View File

@ -0,0 +1,11 @@
export interface IGeodesic {
inverse(
lon1: number,
lat1: number,
lon2: number,
lat2: number,
): {
azimuth: number;
distance: number;
};
}

View File

@ -0,0 +1,5 @@
import { Point } from '../types/point.type';
export interface IRequestGeography {
waypoints: Array<Point>;
}

View File

@ -0,0 +1,5 @@
import { IGeorouter } from './georouter.interface';
export interface ICreateGeorouter {
create(type: string, url: string): IGeorouter;
}

View File

@ -0,0 +1,10 @@
import { NamedRoute } from '../entities/ecosystem/named-route';
import { GeorouterSettings } from '../types/georouter-settings.type';
import { Path } from '../types/path.type';
export interface IGeorouter {
route(
paths: Array<Path>,
settings: GeorouterSettings,
): Promise<Array<NamedRoute>>;
}

View File

@ -0,0 +1,3 @@
export interface IRequestPerson {
identifier?: number;
}

View File

@ -0,0 +1,4 @@
export interface IRequestRequirement {
seatsDriver?: number;
seatsPassenger?: number;
}

View File

@ -0,0 +1,11 @@
import { MarginDurations } from '../types/margin-durations.type';
import { Schedule } from '../types/schedule.type';
export interface IRequestTime {
departure?: string;
fromDate?: string;
toDate?: string;
schedule?: Schedule;
marginDuration?: number;
marginDurations?: MarginDurations;
}

View File

@ -0,0 +1,9 @@
import { Person } from '../entities/ecosystem/person';
import { Role } from './role.enum';
import { Step } from './step.enum';
export type Actor = {
person: Person;
role: Role;
step: Step;
};

View File

@ -0,0 +1,3 @@
export enum Algorithm {
CLASSIC = 'CLASSIC',
}

View File

@ -0,0 +1,15 @@
import { Algorithm } from './algorithm.enum';
export type DefaultAlgorithmSettings = {
algorithm: Algorithm;
strict: boolean;
remoteness: number;
useProportion: boolean;
proportion: number;
useAzimuth: boolean;
azimuthMargin: number;
maxDetourDistanceRatio: number;
maxDetourDurationRatio: number;
georouterType: string;
georouterUrl: string;
};

View File

@ -0,0 +1,10 @@
import { DefaultAlgorithmSettings } from './default-algorithm-settings.type';
export type IDefaultParams = {
DEFAULT_IDENTIFIER: number;
MARGIN_DURATION: number;
VALIDITY_DURATION: number;
DEFAULT_TIMEZONE: string;
DEFAULT_SEATS: number;
DEFAULT_ALGORITHM_SETTINGS: DefaultAlgorithmSettings;
};

View File

@ -0,0 +1,7 @@
export enum PointType {
HOUSE_NUMBER = 'HOUSE_NUMBER',
STREET_ADDRESS = 'STREET_ADDRESS',
LOCALITY = 'LOCALITY',
VENUE = 'VENUE',
OTHER = 'OTHER',
}

View File

@ -0,0 +1,5 @@
export type GeorouterSettings = {
withPoints: boolean;
withTime: boolean;
withDistance: boolean;
};

View File

@ -0,0 +1,9 @@
export type MarginDurations = {
mon?: number;
tue?: number;
wed?: number;
thu?: number;
fri?: number;
sat?: number;
sun?: number;
};

View File

@ -0,0 +1,6 @@
import { Point } from './point.type';
export type Path = {
key: string;
points: Array<Point>;
};

View File

@ -0,0 +1,7 @@
import { PointType } from './geography.enum';
export type Point = {
lon: number;
lat: number;
type?: PointType;
};

View File

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

View File

@ -0,0 +1,9 @@
export type Schedule = {
mon?: string;
tue?: string;
wed?: string;
thu?: string;
fri?: string;
sat?: string;
sun?: string;
};

View File

@ -0,0 +1,6 @@
export enum Step {
START = 'start',
INTERMEDIATE = 'intermediate',
NEUTRAL = 'neutral',
FINISH = 'finish',
}

View File

@ -0,0 +1,16 @@
export enum TimingFrequency {
FREQUENCY_PUNCTUAL = 1,
FREQUENCY_RECURRENT = 2,
}
export enum TimingDays {
'sun',
'mon',
'tue',
'wed',
'thu',
'fri',
'sat',
}
export const Days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];

View File

@ -0,0 +1,7 @@
import { Actor } from './actor.type.';
import { Point } from './point.type';
export type Waypoint = {
point: Point;
actors: Array<Actor>;
};

View File

@ -0,0 +1,77 @@
import { Mapper } from '@automapper/core';
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';
@QueryHandler(MatchQuery)
export class MatchUseCase {
constructor(
private readonly _repository: AdRepository,
private readonly _messager: Messager,
@InjectMapper() private readonly _mapper: Mapper,
) {}
execute = async (matchQuery: MatchQuery): Promise<ICollection<Match>> => {
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';
this._messager.publish('matcher.match', 'match !');
return {
data: [match],
total: 1,
};
} catch (error) {
const err: Error = error;
this._messager.publish(
'logging.matcher.match.crit',
JSON.stringify({
matchQuery,
error: err.message,
}),
);
throw error;
}
};
}

View File

@ -0,0 +1,13 @@
export class MatcherException implements Error {
name: string;
message: string;
constructor(private _code: number, private _message: string) {
this.name = 'MatcherException';
this.message = _message;
}
get code(): number {
return this._code;
}
}

View File

@ -0,0 +1,18 @@
import { createMap, Mapper } from '@automapper/core';
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import { Injectable } from '@nestjs/common';
import { MatchPresenter } from '../adapters/secondaries/match.presenter';
import { Match } from '../domain/entities/ecosystem/match';
@Injectable()
export class MatchProfile extends AutomapperProfile {
constructor(@InjectMapper() mapper: Mapper) {
super(mapper);
}
override get profile() {
return (mapper: Mapper) => {
createMap(mapper, Match, MatchPresenter);
};
}
}

View File

@ -0,0 +1,63 @@
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { DatabaseModule } from '../database/database.module';
import { MatcherController } from './adapters/primaries/matcher.controller';
import { MatchProfile } from './mappers/match.profile';
import { AdRepository } from './adapters/secondaries/ad.repository';
import { MatchUseCase } from './domain/usecases/match.usecase';
import { Messager } from './adapters/secondaries/messager';
import { CacheModule } from '@nestjs/cache-manager';
import { RedisClientOptions } from '@liaoliaots/nestjs-redis';
import { redisStore } from 'cache-manager-ioredis-yet';
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
import { GeorouterCreator } from './adapters/secondaries/georouter-creator';
import { HttpModule } from '@nestjs/axios';
import { MatcherGeodesic } from './adapters/secondaries/geodesic';
@Module({
imports: [
DatabaseModule,
CqrsModule,
HttpModule,
RabbitMQModule.forRootAsync(RabbitMQModule, {
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
exchanges: [
{
name: configService.get<string>('RMQ_EXCHANGE'),
type: 'topic',
},
],
uri: configService.get<string>('RMQ_URI'),
connectionInitOptions: { wait: false },
}),
inject: [ConfigService],
}),
CacheModule.registerAsync<RedisClientOptions>({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
store: await redisStore({
host: configService.get<string>('REDIS_HOST'),
port: configService.get<number>('REDIS_PORT'),
password: configService.get<string>('REDIS_PASSWORD'),
ttl: configService.get<number>('CACHE_TTL'),
}),
}),
inject: [ConfigService],
}),
],
controllers: [MatcherController],
providers: [
MatchProfile,
AdRepository,
Messager,
DefaultParamsProvider,
MatchUseCase,
GeorouterCreator,
MatcherGeodesic,
],
exports: [],
})
export class MatcherModule {}

View File

@ -0,0 +1,103 @@
import { MatchRequest } from '../domain/dtos/match.request';
import { Geography } from '../domain/entities/ecosystem/geography';
import { Person } from '../domain/entities/ecosystem/person';
import { Requirement } from '../domain/entities/ecosystem/requirement';
import { Role } from '../domain/types/role.enum';
import { AlgorithmSettings } from '../domain/entities/ecosystem/algorithm-settings';
import { Time } from '../domain/entities/ecosystem/time';
import { IDefaultParams } from '../domain/types/default-params.type';
import { IGeorouter } from '../domain/interfaces/georouter.interface';
import { ICreateGeorouter } from '../domain/interfaces/georouter-creator.interface';
export class MatchQuery {
private readonly _matchRequest: MatchRequest;
private readonly _defaultParams: IDefaultParams;
private readonly _georouterCreator: ICreateGeorouter;
person: Person;
roles: Array<Role>;
time: Time;
geography: Geography;
exclusions: Array<number>;
requirement: Requirement;
algorithmSettings: AlgorithmSettings;
georouter: IGeorouter;
constructor(
matchRequest: MatchRequest,
defaultParams: IDefaultParams,
georouterCreator: ICreateGeorouter,
) {
this._matchRequest = matchRequest;
this._defaultParams = defaultParams;
this._georouterCreator = georouterCreator;
this._setPerson();
this._setRoles();
this._setTime();
this._setGeography();
this._setRequirement();
this._setAlgorithmSettings();
this._setExclusions();
}
createRoutes = (): void => {
this.geography.createRoutes(this.roles, this.algorithmSettings.georouter);
};
_setPerson = (): void => {
this.person = new Person(
this._matchRequest,
this._defaultParams.DEFAULT_IDENTIFIER,
this._defaultParams.MARGIN_DURATION,
);
this.person.init();
};
_setRoles = (): void => {
this.roles = [];
if (this._matchRequest.driver) this.roles.push(Role.DRIVER);
if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER);
if (this.roles.length == 0) this.roles.push(Role.PASSENGER);
};
_setTime = (): void => {
this.time = new Time(
this._matchRequest,
this._defaultParams.MARGIN_DURATION,
this._defaultParams.VALIDITY_DURATION,
);
this.time.init();
};
_setGeography = (): void => {
this.geography = new Geography(
this._matchRequest,
this._defaultParams.DEFAULT_TIMEZONE,
this.person,
);
this.geography.init();
};
_setRequirement = (): void => {
this.requirement = new Requirement(
this._matchRequest,
this._defaultParams.DEFAULT_SEATS,
);
};
_setAlgorithmSettings = (): void => {
this.algorithmSettings = new AlgorithmSettings(
this._matchRequest,
this._defaultParams.DEFAULT_ALGORITHM_SETTINGS,
this.time.frequency,
this._georouterCreator,
);
};
_setExclusions = (): void => {
this.exclusions = [];
if (this._matchRequest.identifier)
this.exclusions.push(this._matchRequest.identifier);
if (this._matchRequest.exclusions)
this.exclusions.push(...this._matchRequest.exclusions);
};
}

View File

@ -0,0 +1,38 @@
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider';
import { IDefaultParams } from '../../../../domain/types/default-params.type';
const mockConfigService = {
get: jest.fn().mockImplementationOnce(() => 99),
};
describe('DefaultParamsProvider', () => {
let defaultParamsProvider: DefaultParamsProvider;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
DefaultParamsProvider,
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
defaultParamsProvider = module.get<DefaultParamsProvider>(
DefaultParamsProvider,
);
});
it('should be defined', () => {
expect(defaultParamsProvider).toBeDefined();
});
it('should provide default params', async () => {
const params: IDefaultParams = defaultParamsProvider.getParams();
expect(params.DEFAULT_IDENTIFIER).toBe(99);
});
});

View File

@ -0,0 +1,14 @@
import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic';
describe('Matcher geodesic', () => {
it('should be defined', () => {
const geodesic: MatcherGeodesic = new MatcherGeodesic();
expect(geodesic).toBeDefined();
});
it('should get inverse values', () => {
const geodesic: MatcherGeodesic = new MatcherGeodesic();
const inv = geodesic.inverse(0, 0, 1, 1);
expect(Math.round(inv.azimuth)).toBe(45);
expect(Math.round(inv.distance)).toBe(156900);
});
});

View File

@ -0,0 +1,47 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator';
import { GraphhopperGeorouter } from '../../../../adapters/secondaries/graphhopper-georouter';
import { HttpService } from '@nestjs/axios';
import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic';
const mockHttpService = jest.fn();
const mockMatcherGeodesic = jest.fn();
describe('Georouter creator', () => {
let georouterCreator: GeorouterCreator;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
GeorouterCreator,
{
provide: HttpService,
useValue: mockHttpService,
},
{
provide: MatcherGeodesic,
useValue: mockMatcherGeodesic,
},
],
}).compile();
georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
});
it('should be defined', () => {
expect(georouterCreator).toBeDefined();
});
it('should create a graphhopper georouter', () => {
const georouter = georouterCreator.create(
'graphhopper',
'http://localhost',
);
expect(georouter).toBeInstanceOf(GraphhopperGeorouter);
});
it('should throw an exception if georouter type is unknown', () => {
expect(() =>
georouterCreator.create('unknown', 'http://localhost'),
).toThrow();
});
});

View File

@ -0,0 +1,456 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpService } from '@nestjs/axios';
import { GeorouterCreator } from '../../../../adapters/secondaries/georouter-creator';
import { IGeorouter } from '../../../../domain/interfaces/georouter.interface';
import { of } from 'rxjs';
import { AxiosError } from 'axios';
import { MatcherGeodesic } from '../../../../adapters/secondaries/geodesic';
const mockHttpService = {
get: jest
.fn()
.mockImplementationOnce(() => {
throw new AxiosError('Axios error !');
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 5, 180000],
[5, 6, 180000],
[6, 7, 180000],
[7, 9, 360000],
[9, 10, 180000],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[10, 10],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[5, 5],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 7, 540000],
[7, 9, 360000],
[9, 10, 180000],
],
},
},
],
},
});
})
.mockImplementationOnce(() => {
return of({
status: 200,
data: {
paths: [
{
distance: 50000,
time: 1800000,
points: {
coordinates: [
[0, 0],
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5],
[6, 6],
[7, 7],
[8, 8],
[9, 9],
[10, 10],
],
},
snapped_waypoints: {
coordinates: [
[0, 0],
[5, 5],
[10, 10],
],
},
details: {
time: [
[0, 1, 180000],
[1, 2, 180000],
[2, 3, 180000],
[3, 4, 180000],
[4, 7, 540000],
[7, 9, 360000],
[9, 10, 180000],
],
},
instructions: [
{
distance: 25000,
sign: 0,
interval: [0, 5],
text: 'Some instructions',
time: 900000,
},
{
distance: 0,
sign: 5,
interval: [5, 5],
text: 'Waypoint 1',
time: 0,
},
{
distance: 25000,
sign: 2,
interval: [5, 10],
text: 'Some instructions',
time: 900000,
},
{
distance: 0.0,
sign: 4,
interval: [10, 10],
text: 'Arrive at destination',
time: 0,
},
],
},
],
},
});
}),
};
const mockMatcherGeodesic = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inverse: jest.fn().mockImplementation(() => ({
azimuth: 45,
distance: 50000,
})),
};
describe('Graphhopper Georouter', () => {
let georouterCreator: GeorouterCreator;
let graphhopperGeorouter: IGeorouter;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
GeorouterCreator,
{
provide: HttpService,
useValue: mockHttpService,
},
{
provide: MatcherGeodesic,
useValue: mockMatcherGeodesic,
},
],
}).compile();
georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
graphhopperGeorouter = georouterCreator.create(
'graphhopper',
'http://localhost',
);
});
it('should be defined', () => {
expect(graphhopperGeorouter).toBeDefined();
});
describe('route function', () => {
it('should fail on axios error', async () => {
await expect(
graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 1,
lon: 1,
},
],
},
],
{
withDistance: false,
withPoints: false,
withTime: false,
},
),
).rejects.toBeInstanceOf(Error);
});
it('should create one route with all settings to false', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: false,
withTime: false,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.distance).toBe(50000);
});
it('should create one route with points', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: true,
withTime: false,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.distance).toBe(50000);
expect(routes[0].route.duration).toBe(1800);
expect(routes[0].route.fwdAzimuth).toBe(45);
expect(routes[0].route.backAzimuth).toBe(225);
expect(routes[0].route.points.length).toBe(11);
});
it('should create one route with points and time', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: true,
withTime: true,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.spacetimePoints.length).toBe(2);
expect(routes[0].route.spacetimePoints[1].duration).toBe(1800);
expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined();
});
it('should create one route with points and missed waypoints extrapolations', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 5,
lon: 5,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: false,
withPoints: true,
withTime: true,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.spacetimePoints.length).toBe(3);
expect(routes[0].route.distance).toBe(50000);
expect(routes[0].route.duration).toBe(1800);
expect(routes[0].route.fwdAzimuth).toBe(45);
expect(routes[0].route.backAzimuth).toBe(225);
expect(routes[0].route.points.length).toBe(9);
});
it('should create one route with points, time and distance', async () => {
const routes = await graphhopperGeorouter.route(
[
{
key: 'route1',
points: [
{
lat: 0,
lon: 0,
},
{
lat: 10,
lon: 10,
},
],
},
],
{
withDistance: true,
withPoints: true,
withTime: true,
},
);
expect(routes).toHaveLength(1);
expect(routes[0].route.spacetimePoints.length).toBe(3);
expect(routes[0].route.spacetimePoints[1].duration).toBe(990);
expect(routes[0].route.spacetimePoints[1].distance).toBe(25000);
});
});
});

View File

@ -0,0 +1,47 @@
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../../../adapters/secondaries/messager';
const mockAmqpConnection = {
publish: jest.fn().mockImplementation(),
};
const mockConfigService = {
get: jest.fn().mockResolvedValue({
RMQ_EXCHANGE: 'mobicoop',
}),
};
describe('Messager', () => {
let messager: Messager;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
Messager,
{
provide: AmqpConnection,
useValue: mockAmqpConnection,
},
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
messager = module.get<Messager>(Messager);
});
it('should be defined', () => {
expect(messager).toBeDefined();
});
it('should publish a message', async () => {
jest.spyOn(mockAmqpConnection, 'publish');
messager.publish('test.create.info', 'my-test');
expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,282 @@
import { Person } from '../../../../domain/entities/ecosystem/person';
import {
Geography,
RouteKey,
} from '../../../../domain/entities/ecosystem/geography';
import { Role } from '../../../../domain/types/role.enum';
import { NamedRoute } from '../../../../domain/entities/ecosystem/named-route';
import { Route } from '../../../../domain/entities/ecosystem/route';
import { IGeodesic } from '../../../../domain/interfaces/geodesic.interface';
import { PointType } from '../../../../domain/types/geography.enum';
const person: Person = new Person(
{
identifier: 1,
},
0,
900,
);
const mockGeodesic: IGeodesic = {
inverse: jest.fn().mockImplementation(() => ({
azimuth: 45,
distance: 50000,
})),
};
const mockGeorouter = {
route: jest
.fn()
.mockImplementationOnce(() => {
return [
<NamedRoute>{
key: RouteKey.COMMON,
route: new Route(mockGeodesic),
},
];
})
.mockImplementationOnce(() => {
return [
<NamedRoute>{
key: RouteKey.DRIVER,
route: new Route(mockGeodesic),
},
<NamedRoute>{
key: RouteKey.PASSENGER,
route: new Route(mockGeodesic),
},
];
})
.mockImplementationOnce(() => {
return [
<NamedRoute>{
key: RouteKey.DRIVER,
route: new Route(mockGeodesic),
},
];
})
.mockImplementationOnce(() => {
return [
<NamedRoute>{
key: RouteKey.PASSENGER,
route: new Route(mockGeodesic),
},
];
}),
};
describe('Geography entity', () => {
it('should be defined', () => {
const geography = new Geography(
{
waypoints: [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
],
},
'Europe/Paris',
person,
);
expect(geography).toBeDefined();
});
describe('init', () => {
it('should initialize a geography request with point types', () => {
const geography = new Geography(
{
waypoints: [
{
lat: 49.440041,
lon: 1.093912,
type: PointType.LOCALITY,
},
{
lat: 50.630992,
lon: 3.045432,
type: PointType.LOCALITY,
},
],
},
'Europe/Paris',
person,
);
geography.init();
expect(geography._points.length).toBe(2);
expect(geography.originType).toBe(PointType.LOCALITY);
expect(geography.destinationType).toBe(PointType.LOCALITY);
});
it('should throw an exception if waypoints are empty', () => {
const geography = new Geography(
{
waypoints: [],
},
'Europe/Paris',
person,
);
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,
},
],
},
'Europe/Paris',
person,
);
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,
},
],
},
'Europe/Paris',
person,
);
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,
},
],
},
'Europe/Paris',
person,
);
expect(() => geography.init()).toThrow();
});
});
describe('create route', () => {
it('should create routes as driver and passenger', async () => {
const geography = new Geography(
{
waypoints: [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
],
},
'Europe/Paris',
person,
);
geography.init();
await geography.createRoutes(
[Role.DRIVER, Role.PASSENGER],
mockGeorouter,
);
expect(geography.driverRoute.waypoints.length).toBe(2);
expect(geography.passengerRoute.waypoints.length).toBe(2);
});
it('should create routes as driver and passenger with 3 waypoints', async () => {
const geography = new Geography(
{
waypoints: [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 49.781215,
lon: 2.198475,
},
{
lat: 50.630992,
lon: 3.045432,
},
],
},
'Europe/Paris',
person,
);
geography.init();
await geography.createRoutes(
[Role.DRIVER, Role.PASSENGER],
mockGeorouter,
);
expect(geography.driverRoute.waypoints.length).toBe(3);
expect(geography.passengerRoute.waypoints.length).toBe(2);
});
it('should create routes as driver', async () => {
const geography = new Geography(
{
waypoints: [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
],
},
'Europe/Paris',
person,
);
geography.init();
await geography.createRoutes([Role.DRIVER], mockGeorouter);
expect(geography.driverRoute.waypoints.length).toBe(2);
expect(geography.passengerRoute).toBeUndefined();
});
it('should create routes as passenger', async () => {
const geography = new Geography(
{
waypoints: [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
],
},
'Europe/Paris',
person,
);
geography.init();
await geography.createRoutes([Role.PASSENGER], mockGeorouter);
expect(geography.passengerRoute.waypoints.length).toBe(2);
expect(geography.driverRoute).toBeUndefined();
});
});
});

View File

@ -0,0 +1,40 @@
import { Person } from '../../../../domain/entities/ecosystem/person';
const DEFAULT_IDENTIFIER = 0;
const MARGIN_DURATION = 900;
describe('Person entity', () => {
it('should be defined', () => {
const person = new Person(
{
identifier: 1,
},
DEFAULT_IDENTIFIER,
MARGIN_DURATION,
);
expect(person).toBeDefined();
});
describe('init', () => {
it('should initialize a person with an identifier', () => {
const person = new Person(
{
identifier: 1,
},
DEFAULT_IDENTIFIER,
MARGIN_DURATION,
);
person.init();
expect(person.identifier).toBe(1);
expect(person.marginDurations[0]).toBe(900);
expect(person.marginDurations[6]).toBe(900);
});
it('should initialize a person without an identifier', () => {
const person = new Person({}, DEFAULT_IDENTIFIER, MARGIN_DURATION);
person.init();
expect(person.identifier).toBe(0);
expect(person.marginDurations[0]).toBe(900);
expect(person.marginDurations[6]).toBe(900);
});
});
});

View File

@ -0,0 +1,65 @@
import { Route } from '../../../../domain/entities/ecosystem/route';
import { SpacetimePoint } from '../../../../domain/entities/ecosystem/spacetime-point';
import { Waypoint } from '../../../../domain/entities/ecosystem/waypoint';
const mockGeodesic = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inverse: jest.fn().mockImplementation((lon1, lat1, lon2, lat2) => {
return lon1 == 0
? {
azimuth: 45,
distance: 50000,
}
: {
azimuth: -45,
distance: 60000,
};
}),
};
describe('Route entity', () => {
it('should be defined', () => {
const route = new Route(mockGeodesic);
expect(route).toBeDefined();
});
it('should set waypoints and geodesic values for a route', () => {
const route = new Route(mockGeodesic);
const waypoint1: Waypoint = new Waypoint({
lon: 0,
lat: 0,
});
const waypoint2: Waypoint = new Waypoint({
lon: 10,
lat: 10,
});
route.setWaypoints([waypoint1, waypoint2]);
expect(route.waypoints.length).toBe(2);
expect(route.fwdAzimuth).toBe(45);
expect(route.backAzimuth).toBe(225);
expect(route.distanceAzimuth).toBe(50000);
});
it('should set points and geodesic values for a route', () => {
const route = new Route(mockGeodesic);
route.setPoints([
{
lon: 10,
lat: 10,
},
{
lon: 20,
lat: 20,
},
]);
expect(route.points.length).toBe(2);
expect(route.fwdAzimuth).toBe(315);
expect(route.backAzimuth).toBe(135);
expect(route.distanceAzimuth).toBe(60000);
});
it('should set spacetimePoints for a route', () => {
const route = new Route(mockGeodesic);
const spacetimePoint1 = new SpacetimePoint([0, 0], 0, 0);
const spacetimePoint2 = new SpacetimePoint([10, 10], 500, 5000);
route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]);
expect(route.spacetimePoints.length).toBe(2);
});
});

View File

@ -0,0 +1,186 @@
import { Time } from '../../../../domain/entities/ecosystem/time';
const MARGIN_DURATION = 900;
const VALIDITY_DURATION = 365;
describe('Time entity', () => {
it('should be defined', () => {
const time = new Time(
{
departure: '2023-04-01 12:24:00',
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
expect(time).toBeDefined();
});
describe('init', () => {
it('should initialize a punctual time request', () => {
const time = new Time(
{
departure: '2023-04-01 12:24:00',
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
time.init();
expect(time.fromDate.getFullYear()).toBe(
new Date('2023-04-01 12:24:00').getFullYear(),
);
});
it('should initialize a punctual time request with specific single margin duration', () => {
const time = new Time(
{
departure: '2023-04-01 12:24:00',
marginDuration: 300,
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
time.init();
expect(time.marginDurations['tue']).toBe(300);
});
it('should initialize a punctual time request with specific margin durations', () => {
const time = new Time(
{
departure: '2023-04-01 12:24:00',
marginDurations: {
sat: 350,
},
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
time.init();
expect(time.marginDurations['tue']).toBe(900);
expect(time.marginDurations['sat']).toBe(350);
});
it('should initialize a punctual time request with specific single margin duration and margin durations', () => {
const time = new Time(
{
departure: '2023-04-01 12:24:00',
marginDuration: 500,
marginDurations: {
sat: 350,
},
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
time.init();
expect(time.marginDurations['tue']).toBe(500);
expect(time.marginDurations['sat']).toBe(350);
});
it('should initialize a recurrent time request', () => {
const time = new Time(
{
fromDate: '2023-04-01',
schedule: {
mon: '12:00',
},
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
time.init();
expect(time.fromDate.getFullYear()).toBe(
new Date('2023-04-01').getFullYear(),
);
});
it('should throw an exception if no date is provided', () => {
const time = new Time({}, MARGIN_DURATION, VALIDITY_DURATION);
expect(() => time.init()).toThrow();
});
it('should throw an exception if punctual date is invalid', () => {
const time = new Time(
{
departure: '2023-15-01 12:24:00',
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
expect(() => time.init()).toThrow();
});
it('should throw an exception if recurrent fromDate is invalid', () => {
const time = new Time(
{
fromDate: '2023-15-01',
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
expect(() => time.init()).toThrow();
});
it('should throw an exception if recurrent toDate is invalid', () => {
const time = new Time(
{
fromDate: '2023-04-01',
toDate: '2023-13-01',
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
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(
{
fromDate: '2023-04-01',
toDate: '2024-03-31',
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
expect(() => time.init()).toThrow();
});
it('should throw an exception if schedule is empty', () => {
const time = new Time(
{
fromDate: '2023-04-01',
toDate: '2024-03-31',
schedule: {},
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
expect(() => time.init()).toThrow();
});
it('should throw an exception if schedule is invalid', () => {
const time = new Time(
{
fromDate: '2023-04-01',
toDate: '2024-03-31',
schedule: {
mon: '32:78',
},
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
expect(() => time.init()).toThrow();
});
});
it('should throw an exception if margin durations is provided but empty', () => {
const time = new Time(
{
departure: '2023-04-01 12:24:00',
marginDurations: {},
},
MARGIN_DURATION,
VALIDITY_DURATION,
);
expect(() => time.init()).toThrow();
});
});

View File

@ -0,0 +1,89 @@
import { Test, TestingModule } from '@nestjs/testing';
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';
const mockAdRepository = {};
const mockMessager = {
publish: jest.fn().mockImplementation(),
};
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: Algorithm.CLASSIC,
strict: false,
remoteness: 15000,
useProportion: true,
proportion: 0.3,
useAzimuth: true,
azimuthMargin: 10,
maxDetourDistanceRatio: 0.3,
maxDetourDurationRatio: 0.3,
georouterType: 'graphhopper',
georouterUrl: 'http://localhost',
},
};
describe('MatchUseCase', () => {
let matchUseCase: MatchUseCase;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
providers: [
{
provide: AdRepository,
useValue: mockAdRepository,
},
{
provide: Messager,
useValue: mockMessager,
},
MatchUseCase,
],
}).compile();
matchUseCase = module.get<MatchUseCase>(MatchUseCase);
});
it('should be defined', () => {
expect(matchUseCase).toBeDefined();
});
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);
});
});
});

View File

@ -0,0 +1,210 @@
import { MatchRequest } from '../../../domain/dtos/match.request';
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';
const defaultParams: IDefaultParams = {
DEFAULT_IDENTIFIER: 0,
MARGIN_DURATION: 900,
VALIDITY_DURATION: 365,
DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: {
algorithm: Algorithm.CLASSIC,
strict: false,
remoteness: 15000,
useProportion: true,
proportion: 0.3,
useAzimuth: true,
azimuthMargin: 10,
maxDetourDistanceRatio: 0.3,
maxDetourDurationRatio: 0.3,
georouterType: 'graphhopper',
georouterUrl: 'http://localhost',
},
};
const mockGeorouterCreator = {
create: jest.fn().mockImplementation(),
};
describe('Match query', () => {
it('should be defined', () => {
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,
);
expect(matchQuery).toBeDefined();
});
it('should create a query with excluded identifiers', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.identifier = 125;
matchRequest.exclusions = [126, 127, 128];
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
expect(matchQuery.exclusions.length).toBe(4);
});
it('should create a query with driver role only', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.driver = true;
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
expect(matchQuery.roles).toEqual([Role.DRIVER]);
});
it('should create a query with passenger role only', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.passenger = true;
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
expect(matchQuery.roles).toEqual([Role.PASSENGER]);
});
it('should create a query with driver and passenger roles', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.passenger = true;
matchRequest.driver = true;
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
expect(matchQuery.roles.length).toBe(2);
expect(matchQuery.roles).toContain(Role.PASSENGER);
expect(matchQuery.roles).toContain(Role.DRIVER);
});
it('should create a query with number of seats modified', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.seatsDriver = 1;
matchRequest.seatsPassenger = 2;
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
expect(matchQuery.requirement.seatsDriver).toBe(1);
expect(matchQuery.requirement.seatsPassenger).toBe(2);
});
it('should create a query with modified algorithm settings', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.algorithm = Algorithm.CLASSIC;
matchRequest.strict = true;
matchRequest.useProportion = true;
matchRequest.proportion = 0.45;
matchRequest.useAzimuth = true;
matchRequest.azimuthMargin = 15;
matchRequest.remoteness = 20000;
matchRequest.maxDetourDistanceRatio = 0.41;
matchRequest.maxDetourDurationRatio = 0.42;
const matchQuery: MatchQuery = new MatchQuery(
matchRequest,
defaultParams,
mockGeorouterCreator,
);
expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC);
expect(matchQuery.algorithmSettings.restrict).toBe(
TimingFrequency.FREQUENCY_PUNCTUAL,
);
expect(matchQuery.algorithmSettings.useProportion).toBeTruthy();
expect(matchQuery.algorithmSettings.proportion).toBe(0.45);
expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy();
expect(matchQuery.algorithmSettings.azimuthMargin).toBe(15);
expect(matchQuery.algorithmSettings.remoteness).toBe(20000);
expect(matchQuery.algorithmSettings.maxDetourDistanceRatio).toBe(0.41);
expect(matchQuery.algorithmSettings.maxDetourDurationRatio).toBe(0.42);
});
});

View File

@ -1,6 +1,6 @@
import { ArgumentMetadata } from '@nestjs/common';
import { UpdateTerritoryRequest } from '../../../modules/territory/domain/dtos/update-territory.request';
import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe';
import { MatchRequest } from '../../../matcher/domain/dtos/match.request';
describe('RpcValidationPipe', () => {
it('should not validate request', async () => {
@ -10,13 +10,11 @@ describe('RpcValidationPipe', () => {
});
const metadata: ArgumentMetadata = {
type: 'body',
metatype: UpdateTerritoryRequest,
metatype: MatchRequest,
data: '',
};
await target
.transform(<UpdateTerritoryRequest>{}, metadata)
.catch((err) => {
expect(err.message).toEqual('Rpc Exception');
});
await target.transform(<MatchRequest>{}, metadata).catch((err) => {
expect(err.message).toEqual('Rpc Exception');
});
});
});