From b20d3a21d5ffb6e09eca206b4ffb5d982823a5bc Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 11:59:48 +0200 Subject: [PATCH 01/26] =?UTF-8?q?=F0=9F=94=A7=20Configuration:=20Disable?= =?UTF-8?q?=20padding-line-between-statements=20rule=20in=20ESLint=20confi?= =?UTF-8?q?guration=20for=20clear=20code=20clustering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.cjs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/eslint.config.cjs b/eslint.config.cjs index 0a97e3a..3495815 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -38,13 +38,7 @@ module.exports = [ ], }, ], - 'padding-line-between-statements': [ - 'error', - { blankLine: 'always', prev: 'const', next: '*' }, - { blankLine: 'never', prev: 'const', next: 'const' }, - { blankLine: 'always', prev: 'let', next: '*' }, - { blankLine: 'never', prev: 'let', next: 'let' }, - ], + 'padding-line-between-statements': 0, '@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/ban-types': 0, '@typescript-eslint/default-param-last': 0, From 0534d833c197571d19306b9ab3364514b0405a79 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:00:25 +0200 Subject: [PATCH 02/26] =?UTF-8?q?=F0=9F=94=A7=20Configuration:=20Add=20lin?= =?UTF-8?q?t=20script=20and=20update=20dependencies=20for=20improved=20fun?= =?UTF-8?q?ctionality=20(moment,=20faker)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1381 +++------------------------------------------ package.json | 5 +- 2 files changed, 96 insertions(+), 1290 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad9517c..0981d7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,15 +21,14 @@ "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", - "@softarc/native-federation-node": "^2.0.10", "axios": "^1.6.0", "es-module-shims": "^1.5.12", + "moment": "^2.30.1", "reflect-metadata": "^0.1.13", "rxjs": "~7.8.0", "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-architects/native-federation": "^19.0.2", "@angular-devkit/build-angular": "~19.0.0", "@angular-devkit/core": "~19.0.0", "@angular-devkit/schematics": "~19.0.0", @@ -37,7 +36,9 @@ "@angular/compiler-cli": "~19.0.0", "@angular/language-service": "~19.0.0", "@aposin/ng-aquila": "^18.6.1", + "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.8.0", + "@faker-js/faker": "^9.6.0", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.0.2", "@ngrx/signals": "^19.0.0", @@ -62,6 +63,7 @@ "eslint": "^9.8.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-html": "^8.1.2", + "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-simple-import-sort": "^12.1.1", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -97,24 +99,6 @@ "node": ">=6.0.0" } }, - "node_modules/@angular-architects/native-federation": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@angular-architects/native-federation/-/native-federation-19.0.4.tgz", - "integrity": "sha512-NzhLuwZVU7W48pI9rGKbZsmusbkWcG1aABFwjye6CbT75xPnBNlVQUTxUE2PrRIA1DIj6XXtNn2bkT6TSVHT0g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.19.0", - "@chialab/esbuild-plugin-commonjs": "^0.18.0", - "@softarc/native-federation": "2.0.17", - "@softarc/native-federation-runtime": "2.0.17", - "@types/browser-sync": "^2.29.0", - "browser-sync": "^3.0.2", - "esbuild": "^0.19.5", - "mrmime": "^1.0.1", - "npmlog": "^6.0.2", - "process": "0.11.10" - } - }, "node_modules/@angular-devkit/architect": { "version": "0.1900.7", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1900.7.tgz", @@ -3371,65 +3355,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@chialab/cjs-to-esm": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@chialab/cjs-to-esm/-/cjs-to-esm-0.18.0.tgz", - "integrity": "sha512-fm8X9NhPO5pyUB7gxOZgwxb8lVq1UD4syDJCpqh6x4zGME6RTck7BguWZ4Zgv3GML4fQ4KZtyRwP5eoDgNGrmA==", - "dev": true, - "dependencies": { - "@chialab/estransform": "^0.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/esbuild-plugin-commonjs": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-commonjs/-/esbuild-plugin-commonjs-0.18.0.tgz", - "integrity": "sha512-qZjIsNr1dVEJk6NLyza3pJLHeY7Fz0xjmYteKXElCnlFSKR7vVg6d18AsxVpRnP5qNbvx3XlOvs9U8j97ZQ6bw==", - "dev": true, - "dependencies": { - "@chialab/cjs-to-esm": "^0.18.0", - "@chialab/esbuild-rna": "^0.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/esbuild-rna": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.18.2.tgz", - "integrity": "sha512-ckzskez7bxstVQ4c5cxbx0DRP2teldzrcSGQl2KPh1VJGdO2ZmRrb6vNkBBD5K3dx9tgTyvskWp4dV+Fbg07Ag==", - "dev": true, - "dependencies": { - "@chialab/estransform": "^0.18.0", - "@chialab/node-resolve": "^0.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/estransform": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.18.1.tgz", - "integrity": "sha512-W/WmjpQL2hndD0/XfR0FcPBAUj+aLNeoAVehOjV/Q9bSnioz0GVSAXXhzp59S33ZynxJBBfn8DNiMTVNJmk4Aw==", - "dev": true, - "dependencies": { - "@parcel/source-map": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/node-resolve": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.18.0.tgz", - "integrity": "sha512-eV1m70Qn9pLY9xwFmZ2FlcOzwiaUywsJ7NB/ud8VB7DouvCQtIHkQ3Om7uPX0ojXGEG1LCyO96kZkvbNTxNu0Q==", - "dev": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -4070,6 +3995,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.6.0.tgz", + "integrity": "sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -8012,18 +7953,6 @@ "yargs-parser": "21.1.1" } }, - "node_modules/@parcel/source-map": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", - "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", - "dev": true, - "dependencies": { - "detect-libc": "^1.0.3" - }, - "engines": { - "node": "^12.18.3 || >=14" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", @@ -8910,37 +8839,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true - }, - "node_modules/@softarc/native-federation": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@softarc/native-federation/-/native-federation-2.0.17.tgz", - "integrity": "sha512-62KfvQNUBQheur45e+zaIgkTtIcQY2Xp7qsnxuVTVyv07xKY0y2aSTC3jWX3XnCwnV6do5YVwxlVUWhT4xzgyA==", - "dev": true, - "dependencies": { - "@softarc/native-federation-runtime": "2.0.17", - "json5": "^2.2.0", - "npmlog": "^6.0.2" - } - }, - "node_modules/@softarc/native-federation-node": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@softarc/native-federation-node/-/native-federation-node-2.0.17.tgz", - "integrity": "sha512-JKM1nLeFp3rwQZoqb+ANkyYUj7tX6Bo4JZzzZHpnsEZ6f/atuB3MK8E2rBnADn+fAsbOBjQHnYFfnW9KrswrkQ==" - }, - "node_modules/@softarc/native-federation-runtime": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@softarc/native-federation-runtime/-/native-federation-runtime-2.0.17.tgz", - "integrity": "sha512-FepqL1l5kpcQSVGdcix1SyVY4bfxpWTeLM0EGp+b2B8IJz6YDo0ErEzuMy7VIgCXoBIu895+pjAo+WnbHddiwQ==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - } - }, "node_modules/@swc-node/core": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.13.3.tgz", @@ -9360,78 +9258,6 @@ "@types/node": "*" } }, - "node_modules/@types/browser-sync": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@types/browser-sync/-/browser-sync-2.29.0.tgz", - "integrity": "sha512-d2V8FDX/LbDCSm343N2VChzDxvll0h76I8oSigYpdLgPDmcdcR6fywTggKBkUiDM3qAbHOq7NZvepj/HJM5e2g==", - "dev": true, - "dependencies": { - "@types/micromatch": "^2", - "@types/node": "*", - "@types/serve-static": "*", - "chokidar": "^3.0.0" - } - }, - "node_modules/@types/browser-sync/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/@types/browser-sync/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@types/browser-sync/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@types/browser-sync/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -9451,21 +9277,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -9603,15 +9414,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/micromatch": { - "version": "2.3.35", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-2.3.35.tgz", - "integrity": "sha512-J749bHo/Zu56w0G0NI/IGHLQPiSsjx//0zJhfEVAN95K/xM5C8ZDmhkXtU3qns0sBOao7HuQzr8XV1/2o5LbXA==", - "dev": true, - "dependencies": { - "@types/parse-glob": "*" - } - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -9633,12 +9435,6 @@ "@types/node": "*" } }, - "node_modules/@types/parse-glob": { - "version": "3.0.32", - "resolved": "https://registry.npmjs.org/@types/parse-glob/-/parse-glob-3.0.32.tgz", - "integrity": "sha512-n4xmml2WKR12XeQprN8L/sfiVPa8FHS3k+fxp4kSr/PA2GsGUgFND+bvISJxM0y5QdvzNEGjEVU3eIrcKks/pA==", - "dev": true - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -10479,40 +10275,6 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/are-we-there-yet/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==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -10563,15 +10325,6 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true }, - "node_modules/async-each-series": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", - "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10886,15 +10639,6 @@ } ] }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true, - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -11054,241 +10798,75 @@ "node": ">=8" } }, - "node_modules/browser-sync": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", - "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", - "dev": true, - "dependencies": { - "browser-sync-client": "^3.0.3", - "browser-sync-ui": "^3.0.3", - "bs-recipes": "1.3.4", - "chalk": "4.1.2", - "chokidar": "^3.5.1", - "connect": "3.6.6", - "connect-history-api-fallback": "^1", - "dev-ip": "^1.0.1", - "easy-extender": "^2.3.4", - "eazy-logger": "^4.0.1", - "etag": "^1.8.1", - "fresh": "^0.5.2", - "fs-extra": "3.0.1", - "http-proxy": "^1.18.1", - "immutable": "^3", - "micromatch": "^4.0.8", - "opn": "5.3.0", - "portscanner": "2.2.0", - "raw-body": "^2.3.2", - "resp-modifier": "6.0.2", - "rx": "4.1.0", - "send": "^0.19.0", - "serve-index": "^1.9.1", - "serve-static": "^1.16.2", - "server-destroy": "1.0.1", - "socket.io": "^4.4.1", - "ua-parser-js": "^1.0.33", - "yargs": "^17.3.1" + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { - "browser-sync": "dist/bin.js" + "browserslist": "cli.js" }, "engines": { - "node": ">= 8.0.0" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/browser-sync-client": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", - "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "dependencies": { - "etag": "1.8.1", - "fresh": "0.5.2", - "mitt": "^1.1.3" + "fast-json-stable-stringify": "2.x" }, "engines": { - "node": ">=8.0.0" + "node": ">= 6" } }, - "node_modules/browser-sync-ui": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", - "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "dependencies": { - "async-each-series": "0.1.1", - "chalk": "4.1.2", - "connect-history-api-fallback": "^1", - "immutable": "^3", - "server-destroy": "1.0.1", - "socket.io-client": "^4.4.1", - "stream-throttle": "^0.1.3" + "node-int64": "^0.4.0" } }, - "node_modules/browser-sync/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "bin": { + "btoa": "bin/btoa.js" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">= 0.4.0" } }, - "node_modules/browser-sync/node_modules/fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/browser-sync/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/browser-sync/node_modules/jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/browser-sync/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/browser-sync/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/browser-sync/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bs-recipes": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", - "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", - "dev": true - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "dev": true, - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -11815,15 +11393,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -11968,56 +11537,11 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, - "node_modules/connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -12659,6 +12183,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, + "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -12698,18 +12223,6 @@ "node": ">= 4.0.0" } }, - "node_modules/dev-ip": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", - "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", - "dev": true, - "bin": { - "dev-ip": "lib/dev-ip.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -12866,30 +12379,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, - "node_modules/easy-extender": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", - "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", - "dev": true, - "dependencies": { - "lodash": "^4.17.10" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/eazy-logger": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", - "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", - "dev": true, - "dependencies": { - "chalk": "4.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -12981,134 +12470,6 @@ "once": "^1.4.0" } }, - "node_modules/engine.io": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", - "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", - "dev": true, - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", - "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", @@ -13251,6 +12612,7 @@ "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, + "optional": true, "bin": { "esbuild": "bin/esbuild" }, @@ -13435,6 +12797,18 @@ "node": ">=16.0.0" } }, + "node_modules/eslint-plugin-jest-formatting": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-3.1.0.tgz", + "integrity": "sha512-XyysraZ1JSgGbLSDxjj5HzKKh0glgWf+7CkqxbTqb7zEhW7X2WHo5SBQ8cGhnszKN+2Lj3/oevBlHNbHezoc/A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=0.8.0" + } + }, "node_modules/eslint-plugin-simple-import-sort": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", @@ -14079,69 +13453,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.1", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/finalhandler/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/find-cache-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", @@ -14571,61 +13882,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/gauge/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -14952,12 +14208,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -15357,15 +14607,6 @@ "node": ">=0.10.0" } }, - "node_modules/immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -15642,15 +14883,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-like": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", - "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", - "dev": true, - "dependencies": { - "lodash.isfinite": "^3.3.2" - } - }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -17108,12 +16340,6 @@ "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "dev": true - }, "node_modules/lines-and-columns": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", @@ -17276,12 +16502,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, - "node_modules/lodash.isfinite": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", - "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -17878,12 +17098,6 @@ "node": ">= 18" } }, - "node_modules/mitt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", - "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", - "dev": true - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -17895,13 +17109,12 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "dev": true, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/ms": { @@ -18451,22 +17664,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -18811,27 +18008,6 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", - "dev": true, - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/opn/node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -19394,36 +18570,13 @@ "lodash": "^4.17.14" } }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/portscanner": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", - "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", - "dev": true, - "dependencies": { - "async": "^2.6.0", - "is-number-like": "^1.0.3" - }, - "engines": { - "node": ">=0.4", - "npm": ">=1.0.0" - } - }, - "node_modules/portscanner/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "dependencies": { - "lodash": "^4.17.14" + "ms": "^2.1.1" } }, "node_modules/postcss": { @@ -20116,15 +19269,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -20582,56 +19726,6 @@ "node": ">=10" } }, - "node_modules/resp-modifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", - "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", - "dev": true, - "dependencies": { - "debug": "^2.2.0", - "minimatch": "^3.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/resp-modifier/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/resp-modifier/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/resp-modifier/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/resp-modifier/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -20810,12 +19904,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", - "dev": true - }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -21022,45 +20110,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", - "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -21206,18 +20255,6 @@ "node": ">= 0.8" } }, - "node_modules/server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", - "dev": true - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -21418,151 +20455,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "dev": true, - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -21800,22 +20692,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-throttle": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", - "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", - "dev": true, - "dependencies": { - "commander": "^2.2.0", - "limiter": "^1.0.5" - }, - "bin": { - "throttleproxy": "bin/throttleproxy.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -22862,32 +21738,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -24221,44 +23071,6 @@ "node": ">= 8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wide-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -24425,15 +23237,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 880f3a4..2e43dac 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "start": "npx nx run-many --target=serve --all", "start:shell": "nx run shell:serve", "start:bff": "nx run bff:serve", - "test": "nx run-many -t test --coverage" + "test": "nx run-many -t test --coverage", + "lint": "nx run-many -t lint" }, "private": true, "dependencies": { @@ -25,6 +26,7 @@ "@nestjs/platform-express": "^10.0.2", "axios": "^1.6.0", "es-module-shims": "^1.5.12", + "moment": "^2.30.1", "reflect-metadata": "^0.1.13", "rxjs": "~7.8.0", "zone.js": "~0.15.0" @@ -39,6 +41,7 @@ "@aposin/ng-aquila": "^18.6.1", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.8.0", + "@faker-js/faker": "^9.6.0", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.0.2", "@ngrx/signals": "^19.0.0", From f9ea8b9a26a3bec474e84554d9809d51127f24fc Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:02:09 +0200 Subject: [PATCH 03/26] =?UTF-8?q?=F0=9F=94=A7=20Configuration:=20Add=20esM?= =?UTF-8?q?oduleInterop=20to=20TypeScript=20configuration=20for=20improved?= =?UTF-8?q?=20module=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/libs/input-lib/tsconfig.json | 3 ++- shared/validations/tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/libs/input-lib/tsconfig.json b/frontend/libs/input-lib/tsconfig.json index fde35ea..2898c84 100644 --- a/frontend/libs/input-lib/tsconfig.json +++ b/frontend/libs/input-lib/tsconfig.json @@ -6,7 +6,8 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, }, "files": [], "include": [], diff --git a/shared/validations/tsconfig.json b/shared/validations/tsconfig.json index 6f7169a..6adb3c1 100644 --- a/shared/validations/tsconfig.json +++ b/shared/validations/tsconfig.json @@ -7,7 +7,8 @@ "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true + "noPropertyAccessFromIndexSignature": true, + "esModuleInterop": true }, "files": [], "include": [], From c211390aede8931478897eacd425ec93b1e3c46f Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:02:49 +0200 Subject: [PATCH 04/26] =?UTF-8?q?=E2=9C=A8=20Birthdate=20Implementation:?= =?UTF-8?q?=20Add=20DatepickerComponent=20with=20template,=20styles,=20and?= =?UTF-8?q?=20tests=20for=20date=20input=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/datepicker.component.spec.ts | 52 ++++++++++++++++ .../lib/date-picker/datepicker.component.html | 18 ++++++ .../lib/date-picker/datepicker.component.ts | 59 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts create mode 100644 frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html create mode 100644 frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts diff --git a/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts b/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts new file mode 100644 index 0000000..522f116 --- /dev/null +++ b/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts @@ -0,0 +1,52 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NxDateAdapter } from '@aposin/ng-aquila/datefield'; +import moment from 'moment/moment'; + +import { DatepickerComponent } from '../datepicker.component'; + +describe('DatepickerComponent', () => { + let component: DatepickerComponent; + let fixture: ComponentFixture; + let dateAdapter: NxDateAdapter; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DatepickerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DatepickerComponent); + component = fixture.componentInstance; + + dateAdapter = TestBed.inject(NxDateAdapter); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set the input value correctly', () => { + const testDate = '2000-01-01'; + component.value = testDate; + fixture.detectChanges(); + + const parsedDate = dateAdapter.parse(testDate, 'YYYY-MM-DD', true); + expect(component['geburtsdatum']()).toEqual(parsedDate); + }); + + it('should emit dateChange event when date input changes', () => { + const dateChangeSpy = jest.spyOn(component.dateChange, 'emit'); + + const mockDate = moment('2000-01-01'); + const mockEvent = { + target: { + value: mockDate, + }, + }; + + component.updateDate(mockEvent as any); + + expect(dateChangeSpy).toHaveBeenCalledWith('2000-01-01'); + }); +}); diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html new file mode 100644 index 0000000..9629e30 --- /dev/null +++ b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html @@ -0,0 +1,18 @@ + + + MM/DD/YYYY + Please enter a valid date. + + + diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts new file mode 100644 index 0000000..62a36a1 --- /dev/null +++ b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts @@ -0,0 +1,59 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + inject, + Input, + Output, + signal, +} from '@angular/core'; +import { NxErrorComponent } from '@aposin/ng-aquila/base'; +import { + NxDateAdapter, + NxDatefieldDirective, + NxDatepickerComponent, + NxDatepickerInputEvent, + NxDatepickerToggleComponent, +} from '@aposin/ng-aquila/datefield'; +import { NxFormfieldComponent } from '@aposin/ng-aquila/formfield'; +import { NxInputModule } from '@aposin/ng-aquila/input'; +import { NxMomentDateModule } from '@aposin/ng-aquila/moment-date-adapter'; +import moment from 'moment/moment'; + +@Component({ + selector: 'lib-date-picker', + standalone: true, + imports: [ + CommonModule, + NxInputModule, + NxMomentDateModule, + NxDatefieldDirective, + NxDatepickerComponent, + NxDatepickerToggleComponent, + NxErrorComponent, + NxFormfieldComponent, + ], + templateUrl: './datepicker.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DatepickerComponent { + // TODO: force input date format to DD-MM-YYYY + @Input() + set value(vle: string) { + this.geburtsdatum.set(this.adapter.parse(vle, 'YYYY-MM-DD', true)); + } + + @Output() dateChange: EventEmitter = new EventEmitter(); + + protected readonly adapter = inject(NxDateAdapter); + + protected geburtsdatum = signal(''); + + protected minDate = moment().subtract(100, 'years'); + protected maxDate = moment().subtract(18, 'years'); + + updateDate($event: NxDatepickerInputEvent): void { + this.dateChange.emit($event.target.value?.format('YYYY-MM-DD')); + } +} From 6649642f67d375cf534878196fd525e51e60be2c Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:04:17 +0200 Subject: [PATCH 05/26] =?UTF-8?q?=E2=9C=A8=20Birthdate=20Implementation:?= =?UTF-8?q?=20Integrate=20DatepickerComponent=20into=20input=20form=20with?= =?UTF-8?q?=20enhance=20error=20handling=20for=20date=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/input-lib.component.spec.ts | 1 + .../lib/input-lib/input-lib.component.html | 32 +++++++++++++------ .../src/lib/input-lib/input-lib.component.ts | 15 +++++++-- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts index e36e65e..10d5e25 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts @@ -43,6 +43,7 @@ describe('InputLibComponent', () => { http: mockHttp as HttpClient }; const mockState: InputState = { + geburtsdatum: { value: '2002-02-04', valid: true, error: null }, leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, beitrag: { value: 1000, valid: true, error: null }, berechnungDerLaufzeit: { value: 'Alter bei Rentenbeginn', valid: true, error: null }, diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html index d983d24..80a3812 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html @@ -4,9 +4,21 @@
- +
+ @if (inputStore.uiState().geburtsdatum.error) { +
+
+ + {{ inputStore.uiState().geburtsdatum.error }} + +
+
+ }
@@ -14,7 +26,7 @@
+ (valueChange)="updateInputs({key: 'leistungsVorgabe', value: $event})"> Beitrag Einmalbeitrag Garantierte Mindestrente @@ -27,7 +39,7 @@
+ (input)="updateInputs({key: 'beitrag', value: +$any($event.target).value})" />
@@ -47,7 +59,7 @@
+ (valueChange)="updateInputs({key: 'berechnungDerLaufzeit', value: $event})"> Alter bei Rentenbeginn Aufschubdauer @@ -56,7 +68,7 @@
+ (input)="updateInputs({key: 'laufzeit', value: +$any($event.target).value})" />
@@ -75,7 +87,7 @@
+ (valueChange)="updateInputs({key: 'beitragszahlungsweise', value: $event})"> Einmalbeitrag Monatliche Beiträge @@ -87,7 +99,7 @@
+ (valueChange)="updateInputs({key: 'rentenzahlungsweise', value: $event})"> Monatliche Renten Vierteljährliche Renten Halbjährliche Renten @@ -107,10 +119,12 @@
- Einmalbeitrag: {{ inputStore.uiState().quote.beitrag.einmalbeitrag | currency : 'EUR' }} + Einmalbeitrag: {{ inputStore.uiState().quote.beitrag.einmalbeitrag | currency : 'EUR' }} - Garantierte Mindestrente: {{ inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente | currency : 'EUR' }} + Garantierte Mindestrente: {{ inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente | currency : 'EUR' }}
diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index 92155a0..2d0109b 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -2,11 +2,19 @@ import { CommonModule } from '@angular/common'; import { Component, inject } from '@angular/core'; import { NxErrorModule } from '@aposin/ng-aquila/base'; import { NxButtonModule } from '@aposin/ng-aquila/button'; -import { NxDropdownComponent, NxDropdownItemComponent } from '@aposin/ng-aquila/dropdown'; +import { + NxDropdownComponent, + NxDropdownItemComponent, +} from '@aposin/ng-aquila/dropdown'; import { NxFormfieldComponent } from '@aposin/ng-aquila/formfield'; -import { NxColComponent, NxLayoutComponent, NxRowComponent } from '@aposin/ng-aquila/grid'; +import { + NxColComponent, + NxLayoutComponent, + NxRowComponent, +} from '@aposin/ng-aquila/grid'; import { NxInputModule } from '@aposin/ng-aquila/input'; +import { DatepickerComponent } from '../date-picker/datepicker.component'; import { InputStore } from './store/input.store'; import { Input } from './store/input.store.interfaces'; @@ -24,6 +32,7 @@ import { Input } from './store/input.store.interfaces'; NxInputModule, NxErrorModule, NxButtonModule, + DatepickerComponent, ], templateUrl: './input-lib.component.html', }) @@ -35,6 +44,6 @@ export class InputLibComponent { } async calculate(): Promise { - await this.inputStore.calculate() + await this.inputStore.calculate(); } } From 36668bd93084be9a5e3cdddd2e5a71d087bc80ea Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:05:40 +0200 Subject: [PATCH 06/26] =?UTF-8?q?=E2=9C=A8=20Birthdate=20Implementation:?= =?UTF-8?q?=20Update=20quote=20service=20tests=20to=20include=20geburtsdat?= =?UTF-8?q?um=20in=20quote=20request=20and=20improve=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/__tests__/quote.service.spec.ts | 34 +++++++++++-------- .../services/__tests__/quote.servicespec.ts | 13 ------- 2 files changed, 20 insertions(+), 27 deletions(-) delete mode 100644 frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.servicespec.ts diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts index 95b7adf..92d5f27 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts @@ -12,10 +12,7 @@ describe('QuoteService', () => { beforeEach(() => { httpClient = { post: jest.fn() }; TestBed.configureTestingModule({ - providers: [ - QuoteService, - { provide: HttpClient, useValue: httpClient } - ] + providers: [QuoteService, { provide: HttpClient, useValue: httpClient }], }); service = TestBed.inject(QuoteService); }); @@ -26,13 +23,17 @@ describe('QuoteService', () => { describe('calculateQuote', () => { it('should call the correct endpoint with quote request data', async () => { + const geburtsdatum = '2000-01-01'; + const mockRequest: QuoteRequestDto = { + geburtsdatum, beitrag: 1000, laufzeit: 12, leistungsVorgabe: 'Beitrag', berechnungDerLaufzeit: 'Alter bei Rentenbeginn', - beitragszahlungsweise: 'Monatliche Beiträge' + beitragszahlungsweise: 'Monatliche Beiträge', }; + const mockResponse: QuoteResponseDto = { basisdaten: { geburtsdatum: '1990-01-01', @@ -40,43 +41,48 @@ describe('QuoteService', () => { garantieniveau: '90%', alterBeiRentenbeginn: 67, aufschubdauer: 30, - beitragszahlungsdauer: 10 + beitragszahlungsdauer: 10, }, leistungsmerkmale: { garantierteMindestrente: 50000, einmaligesGarantiekapital: 25000, - todesfallleistungAbAltersrentenbezug: 40000 + todesfallleistungAbAltersrentenbezug: 40000, }, beitrag: { einmalbeitrag: 0, - beitragsdynamik: '1,5%' - } + beitragsdynamik: '1,5%', + }, }; httpClient.post.mockReturnValue(of(mockResponse)); - const response = await firstValueFrom(service.calculateQuote(mockRequest)); + const response = await firstValueFrom( + service.calculateQuote(mockRequest) + ); expect(response).toEqual(mockResponse); expect(httpClient.post).toHaveBeenCalledWith('/api/quote', mockRequest); }); it('should propagate errors from the API', async () => { + const geburtsdatum = '2000-01-01'; + const mockRequest: QuoteRequestDto = { + geburtsdatum, beitrag: 1000, laufzeit: 12, leistungsVorgabe: 'Beitrag', berechnungDerLaufzeit: 'Alter bei Rentenbeginn', - beitragszahlungsweise: 'Monatliche Beiträge' + beitragszahlungsweise: 'Monatliche Beiträge', }; const errorResponse = new Error('API Error'); httpClient.post.mockReturnValue(throwError(() => errorResponse)); - await expect(firstValueFrom(service.calculateQuote(mockRequest))) - .rejects.toBe(errorResponse); + await expect( + firstValueFrom(service.calculateQuote(mockRequest)) + ).rejects.toBe(errorResponse); expect(httpClient.post).toHaveBeenCalledWith('/api/quote', mockRequest); }); }); }); - diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.servicespec.ts b/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.servicespec.ts deleted file mode 100644 index 5531f5a..0000000 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.servicespec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; -import { Observable } from 'rxjs'; - -@Injectable({ providedIn: 'root' }) -export class QuoteService { - private readonly http = inject(HttpClient); - - calculateQuote(quoteDto: QuoteRequestDto): Observable { - return this.http.post('/api/quote', quoteDto); - } -} From 564c4bb4e91bce1129453fb2dd927c60496b575a Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:05:58 +0200 Subject: [PATCH 07/26] =?UTF-8?q?=E2=9C=A8=20Birthdate=20Implementation:?= =?UTF-8?q?=20Add=20geburtsdatum=20field=20to=20InputState=20store=20and?= =?UTF-8?q?=20update=20related=20tests=20for=20improved=20validation=20han?= =?UTF-8?q?dling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../store/__tests__/input.store.spec.ts | 49 ++++++------ .../input-lib/store/input.store.interfaces.ts | 1 + .../src/lib/input-lib/store/input.store.ts | 74 ++++++++++++++++--- 3 files changed, 90 insertions(+), 34 deletions(-) diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts index 547004b..b425fd6 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { fakerEN } from '@faker-js/faker'; import { QuoteResponseDto } from '@target/interfaces'; import { InputDtoSchema } from '@target/validations'; import { of, throwError } from 'rxjs'; @@ -16,24 +17,27 @@ describe('InputStore', () => { let store: any; let quoteService: jest.Mocked; + const date = fakerEN.date.past({ years: 20 }); + const geburtsdatum = `${date.getUTCFullYear()}-${date.getMonth()}-${date.getDate()}`; + const mockQuoteResponse: QuoteResponseDto = { basisdaten: { - geburtsdatum: '1990-01-01', + geburtsdatum, versicherungsbeginn: '2024-01-01', garantieniveau: '90%', alterBeiRentenbeginn: 67, aufschubdauer: 30, - beitragszahlungsdauer: 10 + beitragszahlungsdauer: 10, }, leistungsmerkmale: { garantierteMindestrente: 50000, einmaligesGarantiekapital: 25000, - todesfallleistungAbAltersrentenbezug: 40000 + todesfallleistungAbAltersrentenbezug: 40000, }, beitrag: { einmalbeitrag: 0, - beitragsdynamik: '1,5%' - } + beitragsdynamik: '1,5%', + }, }; beforeEach(() => { @@ -42,9 +46,7 @@ describe('InputStore', () => { } as unknown as jest.Mocked; TestBed.configureTestingModule({ - providers: [ - { provide: QuoteService, useValue: quoteService } - ] + providers: [{ provide: QuoteService, useValue: quoteService }], }); store = TestBed.inject(InputStore); @@ -54,7 +56,9 @@ describe('InputStore', () => { it('should update state when validation succeeds', async () => { const input = { key: 'beitrag', value: 2000 }; - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true }); + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: true, + }); await (store as any).updateInputs(input); @@ -69,8 +73,8 @@ describe('InputStore', () => { (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: false, error: { - errors: [{ path: ['beitrag'], message: 'Beitrag must be positive' }] - } + errors: [{ path: ['beitrag'], message: 'Beitrag must be positive' }], + }, }); await (store as any).updateInputs(input); @@ -83,7 +87,9 @@ describe('InputStore', () => { describe('calculate', () => { it('should update quote when calculation succeeds', async () => { - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true }); + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: true, + }); quoteService.calculateQuote.mockReturnValue(of(mockQuoteResponse)); await (store as any).calculate(); @@ -93,22 +99,21 @@ describe('InputStore', () => { }); it('should handle validation failure', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: false }); - - await (store as any).calculate(); - - expect(consoleSpy).toHaveBeenCalledWith(new Error('Invalid input')); - expect(quoteService.calculateQuote).not.toHaveBeenCalled(); + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: false, + }); - consoleSpy.mockRestore(); + await expect((store as any).calculate()).rejects.toThrowError( + 'Invalid input' + ); }); it('should handle API errors', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true }); + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: true, + }); const error = new Error('API Error'); quoteService.calculateQuote.mockReturnValue(throwError(() => error)); diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts index eb12b30..715bff4 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts @@ -7,6 +7,7 @@ interface InputField { } export interface InputState { + geburtsdatum: InputField; leistungsVorgabe: InputField; beitrag: InputField; berechnungDerLaufzeit: InputField; diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts index e4777a4..67fd17f 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts @@ -8,9 +8,14 @@ import { Input, InputState } from './input.store.interfaces'; import { QuoteService } from './services/quote.service'; const initialState: InputState = { + geburtsdatum: { value: '', valid: true, error: null }, leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, beitrag: { value: 1000, valid: true, error: null }, - berechnungDerLaufzeit: { value: 'Alter bei Rentenbeginn', valid: true, error: null }, + berechnungDerLaufzeit: { + value: 'Alter bei Rentenbeginn', + valid: true, + error: null, + }, laufzeit: { value: 10, valid: true, error: null }, beitragszahlungsweise: { value: 'Einmalbeitrag', valid: true, error: null }, rentenzahlungsweise: { value: 'Monatliche Renten', valid: true, error: null }, @@ -21,16 +26,16 @@ const initialState: InputState = { garantieniveau: '', alterBeiRentenbeginn: 0, aufschubdauer: 0, - beitragszahlungsdauer: 0 + beitragszahlungsdauer: 0, }, leistungsmerkmale: { garantierteMindestrente: 0, einmaligesGarantiekapital: 0, - todesfallleistungAbAltersrentenbezug: 0 + todesfallleistungAbAltersrentenbezug: 0, }, beitrag: { einmalbeitrag: 0, - beitragsdynamik: '' + beitragsdynamik: '', }, }, }; @@ -44,7 +49,9 @@ export const InputStore = signalStore( ...store.uiState(), [input.key]: { value: input.value, valid: true, error: null }, }; - const validationResult = await InputDtoSchema.safeParseAsync(transformUiStateToInputDto(initialNewState)); + const validationResult = await InputDtoSchema.safeParseAsync( + transformUiStateToInputDto(initialNewState) + ); if (validationResult.success) { patchState(store, { uiState: initialNewState }); @@ -54,7 +61,11 @@ export const InputStore = signalStore( const validatedState = validationResult.error.errors.reduce( (state, { path, message }) => ({ ...state, - [path[0]]: { ...state[path[0] as keyof InputState], valid: false, error: message }, + [path[0]]: { + ...state[path[0] as keyof InputState], + valid: false, + error: message, + }, }), initialNewState ); @@ -65,12 +76,44 @@ export const InputStore = signalStore( const quoteDto = transformUiStateToInputDto(store.uiState()); const validationResult = await InputDtoSchema.safeParseAsync(quoteDto); - try { - if (!validationResult.success) { - throw new Error('Invalid input'); - } + if (!validationResult.success) { + const validatedState = { ...store.uiState() }; + + // Modify the state for all keys to reset errors tpo ensure and fresh validation + Object.keys(validatedState).forEach((key) => { + patchState(store, { + uiState: { + ...validatedState, + [key]: { + ...validatedState[key as keyof InputState], + valid: true, + error: null, + }, + }, + }); + }); - const quote = await lastValueFrom(quoteService.calculateQuote(quoteDto as QuoteRequestDto)); + // Modify the state for the keys that have errors + validationResult.error?.errors.forEach(({ path, message }) => { + patchState(store, { + uiState: { + ...validatedState, + [path[0]]: { + ...validatedState[path[0] as keyof InputState], + valid: false, + error: message, + }, + }, + }); + }); + + throw new Error('Invalid input'); + } + + try { + const quote = await lastValueFrom( + quoteService.calculateQuote(quoteDto as QuoteRequestDto) + ); patchState(store, { uiState: { ...store.uiState(), quote } }); } catch (error) { @@ -80,4 +123,11 @@ export const InputStore = signalStore( })) ); -const transformUiStateToInputDto = (state: InputState): QuoteRequestDto => Object.entries(state).reduce((acc, [key, { value }]) => ({ ...acc, [key]: value }), {} as QuoteRequestDto); +const transformUiStateToInputDto = (state: InputState): QuoteRequestDto => + Object.entries(state).reduce( + (acc, [key, { value }]) => ({ + ...acc, + [key]: value, + }), + {} as QuoteRequestDto + ); From 7659fafe9835d859df8f81a2b179146ae37459c0 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:06:20 +0200 Subject: [PATCH 08/26] =?UTF-8?q?=E2=9C=A8=20Birthdate=20Implementation:?= =?UTF-8?q?=20Add=20geburtsdatum=20validation=20schema=20to=20enforce=20ag?= =?UTF-8?q?e=20requirement=20and=20date=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../validations/src/lib/input/geburtsdatum.ts | 18 ++++++++++++++++++ .../src/lib/input/input.validations.ts | 2 ++ 2 files changed, 20 insertions(+) create mode 100644 shared/validations/src/lib/input/geburtsdatum.ts diff --git a/shared/validations/src/lib/input/geburtsdatum.ts b/shared/validations/src/lib/input/geburtsdatum.ts new file mode 100644 index 0000000..1dbcf1d --- /dev/null +++ b/shared/validations/src/lib/input/geburtsdatum.ts @@ -0,0 +1,18 @@ +import moment from 'moment/moment'; +import { z } from 'zod'; + +export const geburtsdatumSchema = z.coerce + .string() + .min(1, { message: 'Date of birth is required' }) + .refine((val) => { + if (!val) return true; + + return moment(val).isValid(); + }, 'Please provide a valid date') + .refine((val) => { + const date = moment(val); + + if (!date.isValid()) return true; + + return date.isBefore(moment().subtract(18, 'years')); + }, 'You must be at least 18 years old to proceed'); diff --git a/shared/validations/src/lib/input/input.validations.ts b/shared/validations/src/lib/input/input.validations.ts index 350b841..04892c8 100644 --- a/shared/validations/src/lib/input/input.validations.ts +++ b/shared/validations/src/lib/input/input.validations.ts @@ -2,10 +2,12 @@ import { z } from 'zod'; import { BeitragszahlungsweiseSchema } from './beitragszahlungsweise'; import { BerechnungDerLaufzeitSchema } from './berechnung-der-laufzeit'; +import { geburtsdatumSchema } from './geburtsdatum'; import { LeistungsvorgabeSchema } from './leistungsvorgabe'; import { RentenzahlungsweiseSchema } from './rentenzahlungsweise'; export const InputDtoSchema = z.object({ + geburtsdatum: geburtsdatumSchema, leistungsVorgabe: LeistungsvorgabeSchema.nullish(), beitrag: z .number() From 98fe492463d95fa9855630eeacda552247593cec Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:06:35 +0200 Subject: [PATCH 09/26] =?UTF-8?q?=E2=9C=A8=20Birthdate=20Implementation:?= =?UTF-8?q?=20Update=20backend=20getQuote=20test=20to=20validate=20geburts?= =?UTF-8?q?datum=20and=20ensure=20proper=20quote=20processing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/__tests__/app.controller.spec.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/apps/bff/src/app/__tests__/app.controller.spec.ts b/backend/apps/bff/src/app/__tests__/app.controller.spec.ts index 5683011..a989766 100644 --- a/backend/apps/bff/src/app/__tests__/app.controller.spec.ts +++ b/backend/apps/bff/src/app/__tests__/app.controller.spec.ts @@ -21,29 +21,37 @@ describe('AppController', () => { }); describe('getQuote', () => { - it('should call quoteService.getQuote with the provided DTO', async () => { + it('should call process quote with a valid birthday', async () => { + const geburtsdatum = '2020-01-01'; + const mockQuoteDto: QuoteRequestDto = { - // Add required properties based on your DTO + geburtsdatum, + leistungsVorgabe: 'Beitrag', + beitrag: 1000, + berechnungDerLaufzeit: 'Alter bei Rentenbeginn', + laufzeit: 10, + beitragszahlungsweise: 'Einmalbeitrag', rentenzahlungsweise: 'Monatliche Renten', } as QuoteRequestDto; + const expectedResult = { basisdaten: { - geburtsdatum: '1990-01-01', + geburtsdatum, versicherungsbeginn: '2024-01-01', garantieniveau: '100%', alterBeiRentenbeginn: 67, aufschubdauer: 30, - beitragszahlungsdauer: 30 + beitragszahlungsdauer: 30, }, leistungsmerkmale: { garantierteMindestrente: 1000, einmaligesGarantiekapital: 50000, - todesfallleistungAbAltersrentenbezug: 40000 + todesfallleistungAbAltersrentenbezug: 40000, }, beitrag: { einmalbeitrag: 50000, - beitragsdynamik: '3%' - } + beitragsdynamik: '3%', + }, }; quoteService.getQuote.mockResolvedValue(expectedResult); From 73c24d30998671e9337a6e911ec372da4b517090 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 12:06:54 +0200 Subject: [PATCH 10/26] =?UTF-8?q?=E2=9C=A8=20Birthdate=20Implementation:?= =?UTF-8?q?=20Update=20backend=20getQuote=20method=20to=20include=20geburt?= =?UTF-8?q?sdatum=20in=20request=20and=20response=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quote/__tests__/quote.service.spec.ts | 25 ++++++++++++------- .../src/app/services/quote/quote.service.ts | 15 ++++++----- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts b/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts index 3adec3a..93854f0 100644 --- a/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts +++ b/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts @@ -15,26 +15,33 @@ describe('QuoteService', () => { describe('getQuote', () => { it('should return quote details for given contribution amount', async () => { + const geburtsdatum = '2000-01-01'; + + const beitrag = 1000; + const expectedQuote = { basisdaten: { - geburtsdatum: '1990-01-01', + geburtsdatum: geburtsdatum, versicherungsbeginn: '2025-02-01', garantieniveau: '90%', alterBeiRentenbeginn: 67, aufschubdauer: 30, - beitragszahlungsdauer: 10 + beitragszahlungsdauer: 10, }, leistungsmerkmale: { - garantierteMindestrente: 50000, - einmaligesGarantiekapital: 500, - todesfallleistungAbAltersrentenbezug: 67 + garantierteMindestrente: beitrag * 50, + einmaligesGarantiekapital: beitrag / 2, + todesfallleistungAbAltersrentenbezug: 67, }, beitrag: { - einmalbeitrag: 1000, - beitragsdynamik: '1,5%' - } + einmalbeitrag: beitrag, + beitragsdynamik: '1,5%', + }, }; - const result = await service.getQuote({ beitrag: 1000 }); + const result = await service.getQuote({ + beitrag, + geburtsdatum, + }); expect(result).toEqual(expectedQuote); }); diff --git a/backend/apps/bff/src/app/services/quote/quote.service.ts b/backend/apps/bff/src/app/services/quote/quote.service.ts index 2e99452..5f054b0 100644 --- a/backend/apps/bff/src/app/services/quote/quote.service.ts +++ b/backend/apps/bff/src/app/services/quote/quote.service.ts @@ -3,27 +3,30 @@ import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; @Injectable() export class QuoteService { - async getQuote({ beitrag }: QuoteRequestDto): Promise { + async getQuote({ + beitrag, + geburtsdatum, + }: QuoteRequestDto): Promise { await this.sleep(Math.random() * 4000); // Simulate a real quote service delay 😅 return { basisdaten: { - geburtsdatum: '1990-01-01', + geburtsdatum: geburtsdatum, versicherungsbeginn: '2025-02-01', garantieniveau: '90%', alterBeiRentenbeginn: 67, aufschubdauer: 30, - beitragszahlungsdauer: 10 + beitragszahlungsdauer: 10, }, leistungsmerkmale: { garantierteMindestrente: beitrag * 50, einmaligesGarantiekapital: beitrag / 2, - todesfallleistungAbAltersrentenbezug: 67 + todesfallleistungAbAltersrentenbezug: 67, }, beitrag: { einmalbeitrag: beitrag, - beitragsdynamik: '1,5%' - } + beitragsdynamik: '1,5%', + }, }; } From c362592f3d564eecbc1fb093fe975c0ab80ce3c3 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Wed, 2 Apr 2025 13:59:39 +0200 Subject: [PATCH 11/26] =?UTF-8?q?=E2=9C=A8=20Loading=20State=20Feedback:?= =?UTF-8?q?=20Add=20loading=20spinner=20to=20InputLibComponent=20during=20?= =?UTF-8?q?calculation=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input-lib/__tests__/input-lib.component.spec.ts | 7 ++++--- .../src/lib/input-lib/input-lib.component.html | 11 ++++++++++- .../src/lib/input-lib/input-lib.component.ts | 13 +++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts index 10d5e25..ffb1e64 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts @@ -1,8 +1,9 @@ import { HttpClient, provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; import { of } from 'rxjs'; -import { InputLibComponent } from '../input-lib.component'; +import { InputLibComponent } from '../../../index'; import { InputStore } from '../store/input.store'; import { Input, InputState } from '../store/input.store.interfaces'; import { QuoteService } from '../store/services/quote.service'; @@ -72,7 +73,7 @@ describe('InputLibComponent', () => { }; await TestBed.configureTestingModule({ - imports: [InputLibComponent], + imports: [InputLibComponent,NxSpinnerComponent], providers: [ provideHttpClient(), { provide: QuoteService, useValue: mockQuoteService }, @@ -108,7 +109,7 @@ describe('InputLibComponent', () => { it('should calculate through the store', async () => { await component.calculate(); - + // TOOO: Fix text with "Could not parse CSS stylesheet" error expect(inputStore.calculate).toHaveBeenCalled(); }); diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html index 80a3812..37a51fd 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html @@ -112,7 +112,16 @@
- + @if (isProcessingData()) { + + } + @if (!isProcessingData()) { + + }
@if (inputStore.uiState().quote.beitrag.einmalbeitrag || inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente) { diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index 2d0109b..4444a62 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; import { NxErrorModule } from '@aposin/ng-aquila/base'; import { NxButtonModule } from '@aposin/ng-aquila/button'; import { @@ -13,6 +13,7 @@ import { NxRowComponent, } from '@aposin/ng-aquila/grid'; import { NxInputModule } from '@aposin/ng-aquila/input'; +import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; import { DatepickerComponent } from '../date-picker/datepicker.component'; import { InputStore } from './store/input.store'; @@ -33,17 +34,25 @@ import { Input } from './store/input.store.interfaces'; NxErrorModule, NxButtonModule, DatepickerComponent, + NxSpinnerComponent, ], templateUrl: './input-lib.component.html', }) export class InputLibComponent { protected readonly inputStore = inject(InputStore); + protected isProcessingData = signal(false); + updateInputs(input: Input): void { this.inputStore.updateInputs(input); } async calculate(): Promise { - await this.inputStore.calculate(); + this.isProcessingData.set(true); + + await this.inputStore + .calculate() + .then(() => this.isProcessingData.set(false)) + .catch(() => this.isProcessingData.set(false)); } } From 4bf5bd3eb6cd4d80a3a3dd8cd36c139e2259c062 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Thu, 3 Apr 2025 09:45:58 +0200 Subject: [PATCH 12/26] =?UTF-8?q?=E2=9C=A8=20Infrastructure:=20Generate=20?= =?UTF-8?q?UI=20library=20for=20common=20Angular=20UI=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/libs/ui-lib/README.md | 7 +++++ frontend/libs/ui-lib/eslint.config.cjs | 34 +++++++++++++++++++++++++ frontend/libs/ui-lib/jest.config.ts | 21 +++++++++++++++ frontend/libs/ui-lib/project.json | 20 +++++++++++++++ frontend/libs/ui-lib/src/test-setup.ts | 6 +++++ frontend/libs/ui-lib/tsconfig.json | 28 ++++++++++++++++++++ frontend/libs/ui-lib/tsconfig.lib.json | 17 +++++++++++++ frontend/libs/ui-lib/tsconfig.spec.json | 18 +++++++++++++ tsconfig.base.json | 1 + 9 files changed, 152 insertions(+) create mode 100644 frontend/libs/ui-lib/README.md create mode 100644 frontend/libs/ui-lib/eslint.config.cjs create mode 100644 frontend/libs/ui-lib/jest.config.ts create mode 100644 frontend/libs/ui-lib/project.json create mode 100644 frontend/libs/ui-lib/src/test-setup.ts create mode 100644 frontend/libs/ui-lib/tsconfig.json create mode 100644 frontend/libs/ui-lib/tsconfig.lib.json create mode 100644 frontend/libs/ui-lib/tsconfig.spec.json diff --git a/frontend/libs/ui-lib/README.md b/frontend/libs/ui-lib/README.md new file mode 100644 index 0000000..ea5b39f --- /dev/null +++ b/frontend/libs/ui-lib/README.md @@ -0,0 +1,7 @@ +# ui-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-lib` to execute the unit tests. diff --git a/frontend/libs/ui-lib/eslint.config.cjs b/frontend/libs/ui-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/ui-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/ui-lib/jest.config.ts b/frontend/libs/ui-lib/jest.config.ts new file mode 100644 index 0000000..35dac7d --- /dev/null +++ b/frontend/libs/ui-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'ui-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../coverage/ui-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/ui-lib/project.json b/frontend/libs/ui-lib/project.json new file mode 100644 index 0000000..247b8fe --- /dev/null +++ b/frontend/libs/ui-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "ui-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "ui-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "ui-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/ui-lib/src/test-setup.ts b/frontend/libs/ui-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/ui-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/ui-lib/tsconfig.json b/frontend/libs/ui-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/ui-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/ui-lib/tsconfig.lib.json b/frontend/libs/ui-lib/tsconfig.lib.json new file mode 100644 index 0000000..64216c9 --- /dev/null +++ b/frontend/libs/ui-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/ui-lib/tsconfig.spec.json b/frontend/libs/ui-lib/tsconfig.spec.json new file mode 100644 index 0000000..aad9b06 --- /dev/null +++ b/frontend/libs/ui-lib/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": [ + "src/test-setup.ts" + ], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index f82281f..6e6ff3f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -17,6 +17,7 @@ "paths": { "@target/input-lib": ["frontend/libs/input-lib/src/index.ts"], "@target/interfaces": ["shared/interfaces/src/index.ts"], + "@target/ui-lib": ["frontend/libs/ui-lib/src/index.ts"], "@target/validations": ["shared/validations/src/index.ts"] } }, From a85b4335972ad15a9431c1de6f4bb33740eb41ca Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Thu, 3 Apr 2025 09:46:43 +0200 Subject: [PATCH 13/26] =?UTF-8?q?=E2=9C=A8=20Error=20Handling:=20Add=20err?= =?UTF-8?q?or=20handling=20component=20to=20display=20error=20API=20respon?= =?UTF-8?q?ses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/input-lib/input-lib.component.html | 10 ++-- .../src/lib/input-lib/input-lib.component.ts | 22 +++++-- .../src/lib/input-lib/store/input.store.ts | 59 +++++++++++++++---- frontend/libs/ui-lib/src/index.ts | 1 + .../__tests__/error-box.component.spec.ts | 46 +++++++++++++++ .../ui-lib/error-box/error-box.component.html | 10 ++++ .../ui-lib/error-box/error-box.component.ts | 37 ++++++++++++ shared/interfaces/src/index.ts | 1 + .../src/lib/api-error-response.interface.ts | 9 +++ 9 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 frontend/libs/ui-lib/src/index.ts create mode 100644 frontend/libs/ui-lib/src/lib/ui-lib/error-box/__tests__/error-box.component.spec.ts create mode 100644 frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.html create mode 100644 frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts create mode 100644 shared/interfaces/src/lib/api-error-response.interface.ts diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html index 37a51fd..7143655 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html @@ -4,10 +4,10 @@
- +
@if (inputStore.uiState().geburtsdatum.error) { @@ -124,6 +124,8 @@ }
+ + @if (inputStore.uiState().quote.beitrag.einmalbeitrag || inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente) {
diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index 4444a62..f0e68c9 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -14,6 +14,8 @@ import { } from '@aposin/ng-aquila/grid'; import { NxInputModule } from '@aposin/ng-aquila/input'; import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; +import { ApiErrorResponse } from '@target/interfaces'; +import { ErrorBoxComponent } from '@target/ui-lib'; import { DatepickerComponent } from '../date-picker/datepicker.component'; import { InputStore } from './store/input.store'; @@ -35,6 +37,7 @@ import { Input } from './store/input.store.interfaces'; NxButtonModule, DatepickerComponent, NxSpinnerComponent, + ErrorBoxComponent, ], templateUrl: './input-lib.component.html', }) @@ -42,17 +45,28 @@ export class InputLibComponent { protected readonly inputStore = inject(InputStore); protected isProcessingData = signal(false); + protected errorResponse = signal({} as ApiErrorResponse); updateInputs(input: Input): void { - this.inputStore.updateInputs(input); + this.isProcessingData.set(true); + + this.inputStore + .updateInputs(input) + .then(() => this.isProcessingData.set(false)); } - async calculate(): Promise { + calculate(): void { this.isProcessingData.set(true); - await this.inputStore + this.inputStore .calculate() .then(() => this.isProcessingData.set(false)) - .catch(() => this.isProcessingData.set(false)); + .catch((err) => { + if (err.status === 400) + this.inputStore.processErrors(err.error.message); + else this.errorResponse.set(err.error); + + this.isProcessingData.set(false); + }); } } diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts index 67fd17f..131356e 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts @@ -1,14 +1,14 @@ import { inject } from '@angular/core'; import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; -import { QuoteRequestDto } from '@target/interfaces'; +import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; import { InputDtoSchema } from '@target/validations'; -import { lastValueFrom } from 'rxjs'; +import { lastValueFrom, tap } from 'rxjs'; import { Input, InputState } from './input.store.interfaces'; import { QuoteService } from './services/quote.service'; const initialState: InputState = { - geburtsdatum: { value: '', valid: true, error: null }, + geburtsdatum: { value: '1985-07-25', valid: true, error: null }, leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, beitrag: { value: 1000, valid: true, error: null }, berechnungDerLaufzeit: { @@ -72,7 +72,32 @@ export const InputStore = signalStore( patchState(store, { uiState: validatedState }); }, - calculate: async (): Promise => { + + processErrors: (messages: string[]): void => { + const errorArr = messages?.map((it) => { + const errArr = it.split(':'); + + return { + path: errArr[0].trim(), + message: errArr[1].trim(), + }; + }); + + errorArr?.forEach(({ path, message }) => { + patchState(store, { + uiState: { + ...store.uiState(), + [path]: { + ...store.uiState()[path as keyof InputState], + valid: false, + error: message, + }, + }, + }); + }); + }, + + calculate: async (): Promise => { const quoteDto = transformUiStateToInputDto(store.uiState()); const validationResult = await InputDtoSchema.safeParseAsync(quoteDto); @@ -110,15 +135,25 @@ export const InputStore = signalStore( throw new Error('Invalid input'); } - try { - const quote = await lastValueFrom( - quoteService.calculateQuote(quoteDto as QuoteRequestDto) - ); + return lastValueFrom( + quoteService + .calculateQuote(quoteDto as QuoteRequestDto) + .pipe( + tap((quote) => + patchState(store, { uiState: { ...store.uiState(), quote } }) + ) + ) + ); - patchState(store, { uiState: { ...store.uiState(), quote } }); - } catch (error) { - console.error(error); - } + // try { + // const quote = await lastValueFrom( + // quoteService.calculateQuote(quoteDto as QuoteRequestDto) + // ) + // + // patchState(store, { uiState: { ...store.uiState(), quote } }); + // } catch (error) { + // console.error(error); + // } }, })) ); diff --git a/frontend/libs/ui-lib/src/index.ts b/frontend/libs/ui-lib/src/index.ts new file mode 100644 index 0000000..ef301c5 --- /dev/null +++ b/frontend/libs/ui-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/ui-lib/error-box/error-box.component'; diff --git a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/__tests__/error-box.component.spec.ts b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/__tests__/error-box.component.spec.ts new file mode 100644 index 0000000..4f5629c --- /dev/null +++ b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/__tests__/error-box.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorBoxComponent } from '../../../../index'; + +describe('ErrorBoxComponent', () => { + let component: ErrorBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ErrorBoxComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ErrorBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set simple server error message correctly', () => { + const error = { statusCode: 500, message: 'An error occurred' }; + const expected = { statusCode: 500, message: ['An error occurred'] }; + + component.errorMessage = error; + + expect(component['apiErrorMessage']()).toEqual(expected); + }); + + it('should set composed server error message correctly', () => { + const error = { + statusCode: 500, + message: ['field1: error 1', 'field2: error 2'], + }; + const expected = { + statusCode: 500, + message: ['field1: error 1', 'field2: error 2'], + }; + + component.errorMessage = error; + + expect(component['apiErrorMessage']()).toEqual(expected); + }); +}); diff --git a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.html b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.html new file mode 100644 index 0000000..34a03bc --- /dev/null +++ b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.html @@ -0,0 +1,10 @@ +@if (apiErrorMessage().message.length) { + + The following errors have been reported: +
+
+ {{ item }} +
+
+
+} diff --git a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts new file mode 100644 index 0000000..acfbc7f --- /dev/null +++ b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts @@ -0,0 +1,37 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Input, + signal, +} from '@angular/core'; +import { NxErrorComponent } from '@aposin/ng-aquila/base'; +import { NxFormfieldErrorDirective } from '@aposin/ng-aquila/formfield'; +import { ApiError, ApiErrorResponse } from '@target/interfaces'; + +@Component({ + selector: 'lib-error-box', + imports: [CommonModule, NxErrorComponent, NxFormfieldErrorDirective], + templateUrl: './error-box.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ErrorBoxComponent { + @Input() + set errorMessage(value: ApiErrorResponse) { + if (!value) { + this.apiErrorMessage.set({ message: [] } as ApiError); + } + + const error: ApiError = Array.isArray(value.message) + ? (value as ApiError) + : { + ...value, + message: value.message ? [value.message] : [], + }; + + this.apiErrorMessage.set(error); + } + + protected apiErrorMessage = signal({ message: [] } as ApiError); +} diff --git a/shared/interfaces/src/index.ts b/shared/interfaces/src/index.ts index 3f60786..f5c08a1 100644 --- a/shared/interfaces/src/index.ts +++ b/shared/interfaces/src/index.ts @@ -1 +1,2 @@ +export * from './lib/api-error-response.interface'; export * from './lib/quote.interfaces'; diff --git a/shared/interfaces/src/lib/api-error-response.interface.ts b/shared/interfaces/src/lib/api-error-response.interface.ts new file mode 100644 index 0000000..6b9db1e --- /dev/null +++ b/shared/interfaces/src/lib/api-error-response.interface.ts @@ -0,0 +1,9 @@ +export interface ApiErrorResponse { + message: string | string[]; + statusCode: number; +} + +export interface ApiError { + message: string[]; + statusCode?: number; +} From 54423c723a3a3dff50eb896bec7376ddcfa91db4 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Fri, 4 Apr 2025 12:17:32 +0200 Subject: [PATCH 14/26] =?UTF-8?q?=E2=9C=A8=20Quote=20Summary=20Page:=20Quo?= =?UTF-8?q?te=20state=20store=20decoupling=20from=20quote?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input-lib/store/input.store.interfaces.ts | 3 -- .../src/lib/input-lib/store/input.store.ts | 43 ++----------------- .../input-lib/store/services/quote.service.ts | 6 ++- shared/interfaces/src/lib/quote.interfaces.ts | 4 ++ 4 files changed, 12 insertions(+), 44 deletions(-) diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts index 715bff4..7613430 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts @@ -1,5 +1,3 @@ -import { QuoteResponseDto } from "@target/interfaces"; - interface InputField { value: T; valid: boolean; @@ -14,7 +12,6 @@ export interface InputState { laufzeit: InputField; beitragszahlungsweise: InputField; rentenzahlungsweise: InputField; - quote: QuoteResponseDto; } export interface Input { diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts index 131356e..6133f38 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts @@ -1,8 +1,8 @@ import { inject } from '@angular/core'; import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; +import { QuoteCreateResponseDto, QuoteRequestDto } from '@target/interfaces'; import { InputDtoSchema } from '@target/validations'; -import { lastValueFrom, tap } from 'rxjs'; +import { lastValueFrom } from 'rxjs'; import { Input, InputState } from './input.store.interfaces'; import { QuoteService } from './services/quote.service'; @@ -19,25 +19,6 @@ const initialState: InputState = { laufzeit: { value: 10, valid: true, error: null }, beitragszahlungsweise: { value: 'Einmalbeitrag', valid: true, error: null }, rentenzahlungsweise: { value: 'Monatliche Renten', valid: true, error: null }, - quote: { - basisdaten: { - geburtsdatum: '', - versicherungsbeginn: '', - garantieniveau: '', - alterBeiRentenbeginn: 0, - aufschubdauer: 0, - beitragszahlungsdauer: 0, - }, - leistungsmerkmale: { - garantierteMindestrente: 0, - einmaligesGarantiekapital: 0, - todesfallleistungAbAltersrentenbezug: 0, - }, - beitrag: { - einmalbeitrag: 0, - beitragsdynamik: '', - }, - }, }; export const InputStore = signalStore( @@ -97,7 +78,7 @@ export const InputStore = signalStore( }); }, - calculate: async (): Promise => { + calculate: async (): Promise => { const quoteDto = transformUiStateToInputDto(store.uiState()); const validationResult = await InputDtoSchema.safeParseAsync(quoteDto); @@ -136,24 +117,8 @@ export const InputStore = signalStore( } return lastValueFrom( - quoteService - .calculateQuote(quoteDto as QuoteRequestDto) - .pipe( - tap((quote) => - patchState(store, { uiState: { ...store.uiState(), quote } }) - ) - ) + quoteService.calculateQuote(quoteDto as QuoteRequestDto) ); - - // try { - // const quote = await lastValueFrom( - // quoteService.calculateQuote(quoteDto as QuoteRequestDto) - // ) - // - // patchState(store, { uiState: { ...store.uiState(), quote } }); - // } catch (error) { - // console.error(error); - // } }, })) ); diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts b/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts index 5531f5a..103eaf3 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts @@ -7,7 +7,9 @@ import { Observable } from 'rxjs'; export class QuoteService { private readonly http = inject(HttpClient); - calculateQuote(quoteDto: QuoteRequestDto): Observable { - return this.http.post('/api/quote', quoteDto); + calculateQuote( + quoteDto: QuoteRequestDto + ): Observable { + return this.http.post('/api/quote', quoteDto); } } diff --git a/shared/interfaces/src/lib/quote.interfaces.ts b/shared/interfaces/src/lib/quote.interfaces.ts index ffabedc..a8415a8 100644 --- a/shared/interfaces/src/lib/quote.interfaces.ts +++ b/shared/interfaces/src/lib/quote.interfaces.ts @@ -8,6 +8,10 @@ export interface QuoteResponseDto { beitrag: QuoteBeitragDto; } +export interface QuoteCreateResponseDto { + id: string; +} + interface QuoteBasisdatenDto { geburtsdatum: string; versicherungsbeginn: string; From 1f078e3937b0edad7ab548ee078936a3de7945da Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Fri, 4 Apr 2025 12:20:09 +0200 Subject: [PATCH 15/26] =?UTF-8?q?=E2=9C=A8=20Quote=20Summary=20Page:=20cre?= =?UTF-8?q?ate=20quote=20summary=20component=20lib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/apps/shell/src/app/app.routes.ts | 6 ++ frontend/libs/view-lib/README.md | 7 +++ frontend/libs/view-lib/eslint.config.cjs | 34 +++++++++++ frontend/libs/view-lib/jest.config.ts | 21 +++++++ frontend/libs/view-lib/project.json | 20 +++++++ frontend/libs/view-lib/src/index.ts | 1 + .../__tests__/view-lib.component.spec.ts | 22 +++++++ .../view-lib/store/quote.store.interface.ts | 3 + .../src/lib/view-lib/store/quote.store.ts | 37 ++++++++++++ .../src/lib/view-lib/view-lib.component.html | 58 +++++++++++++++++++ .../src/lib/view-lib/view-lib.component.ts | 29 ++++++++++ frontend/libs/view-lib/src/test-setup.ts | 6 ++ frontend/libs/view-lib/tsconfig.json | 28 +++++++++ frontend/libs/view-lib/tsconfig.lib.json | 17 ++++++ frontend/libs/view-lib/tsconfig.spec.json | 18 ++++++ tsconfig.base.json | 3 +- 16 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 frontend/libs/view-lib/README.md create mode 100644 frontend/libs/view-lib/eslint.config.cjs create mode 100644 frontend/libs/view-lib/jest.config.ts create mode 100644 frontend/libs/view-lib/project.json create mode 100644 frontend/libs/view-lib/src/index.ts create mode 100644 frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts create mode 100644 frontend/libs/view-lib/src/lib/view-lib/store/quote.store.interface.ts create mode 100644 frontend/libs/view-lib/src/lib/view-lib/store/quote.store.ts create mode 100644 frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html create mode 100644 frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts create mode 100644 frontend/libs/view-lib/src/test-setup.ts create mode 100644 frontend/libs/view-lib/tsconfig.json create mode 100644 frontend/libs/view-lib/tsconfig.lib.json create mode 100644 frontend/libs/view-lib/tsconfig.spec.json diff --git a/frontend/apps/shell/src/app/app.routes.ts b/frontend/apps/shell/src/app/app.routes.ts index f60d3a3..9526e55 100644 --- a/frontend/apps/shell/src/app/app.routes.ts +++ b/frontend/apps/shell/src/app/app.routes.ts @@ -1,8 +1,10 @@ import { Route } from '@angular/router'; import { InputLibComponent } from '@target/input-lib'; +import { ViewLibComponent } from '@target/view-lib'; const ROUTES = { INPUTS: 'inputs', + VIEW: 'view', }; export const appRoutes: Route[] = [ @@ -15,6 +17,10 @@ export const appRoutes: Route[] = [ path: ROUTES.INPUTS, component: InputLibComponent, }, + { + path: ROUTES.VIEW, + component: ViewLibComponent, + }, { path: '**', redirectTo: ROUTES.INPUTS, diff --git a/frontend/libs/view-lib/README.md b/frontend/libs/view-lib/README.md new file mode 100644 index 0000000..7b24128 --- /dev/null +++ b/frontend/libs/view-lib/README.md @@ -0,0 +1,7 @@ +# view-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test view-lib` to execute the unit tests. diff --git a/frontend/libs/view-lib/eslint.config.cjs b/frontend/libs/view-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/view-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/view-lib/jest.config.ts b/frontend/libs/view-lib/jest.config.ts new file mode 100644 index 0000000..96f4402 --- /dev/null +++ b/frontend/libs/view-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'view-lib', + preset: '../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../coverage/view-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/view-lib/project.json b/frontend/libs/view-lib/project.json new file mode 100644 index 0000000..195cbd8 --- /dev/null +++ b/frontend/libs/view-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "view-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "view-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "view-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/view-lib/src/index.ts b/frontend/libs/view-lib/src/index.ts new file mode 100644 index 0000000..86fd00c --- /dev/null +++ b/frontend/libs/view-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-lib/view-lib.component'; diff --git a/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts new file mode 100644 index 0000000..105b29b --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ViewLibComponent } from '../view-lib.component'; + +describe('ViewLibComponent', () => { + let component: ViewLibComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ViewLibComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ViewLibComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.interface.ts b/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.interface.ts new file mode 100644 index 0000000..d8c7a49 --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.interface.ts @@ -0,0 +1,3 @@ +import { QuoteResponseDto } from '@target/interfaces'; + +export interface QuoteState extends QuoteResponseDto {} diff --git a/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.ts b/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.ts new file mode 100644 index 0000000..c421b96 --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.ts @@ -0,0 +1,37 @@ +import { inject } from '@angular/core'; +import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { lastValueFrom } from 'rxjs'; + +import { QuoteService } from '../../../../../input-lib/src/lib/input-lib/store/services/quote.service'; +import { QuoteState } from './quote.store.interface'; + +const initialState: QuoteState = { + basisdaten: { + geburtsdatum: '', + versicherungsbeginn: '', + garantieniveau: '', + alterBeiRentenbeginn: 0, + aufschubdauer: 0, + beitragszahlungsdauer: 0, + }, + leistungsmerkmale: { + garantierteMindestrente: 0, + einmaligesGarantiekapital: 0, + todesfallleistungAbAltersrentenbezug: 0, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '', + }, +}; + +export const QuoteStore = signalStore( + { providedIn: 'root' }, + withState({ quoteState: initialState }), + withMethods((store, quoteService = inject(QuoteService)) => ({ + fetchQuote: async (quoteId: string): Promise => { + const quote = await lastValueFrom(quoteService.fetchQuote(quoteId)); + patchState(store, { quoteState: { ...quote } }); + }, + })) +); diff --git a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html new file mode 100644 index 0000000..949cb64 --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html @@ -0,0 +1,58 @@ +
+
+ +
+

Basisdaten

+
+
+ {{ state.basisdaten.geburtsdatum }} +
+
+ {{ state.basisdaten.versicherungsbeginn }} +
+
+ {{ state.basisdaten.garantieniveau }} +
+
+ {{ state.basisdaten.alterBeiRentenbeginn }} +
+
+ {{ state.basisdaten.aufschubdauer }} +
+
+ {{ state.basisdaten.beitragszahlungsdauer }} + +
+
+ +

Leistungsmerkmale

+
+
+ {{ state.leistungsmerkmale.garantierteMindestrente }} + +
+
+ {{ state.leistungsmerkmale.einmaligesGarantiekapital }} + +
+
+ {{ state.leistungsmerkmale.todesfallleistungAbAltersrentenbezug }} + +
+
+ +

Beitrag

+
+
+ {{ state.beitrag.einmalbeitrag }} +
+
+ {{ state.beitrag.beitragsdynamik }} +
+
+
+ +
+
diff --git a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts new file mode 100644 index 0000000..80ca935 --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { NxDataDisplayComponent } from '@aposin/ng-aquila/data-display'; +import { + NxColComponent, + NxLayoutComponent, + NxRowComponent, +} from '@aposin/ng-aquila/grid'; + +import { QuoteStore } from './store/quote.store'; + +@Component({ + selector: 'lib-view-lib', + imports: [ + CommonModule, + NxColComponent, + NxLayoutComponent, + NxRowComponent, + NxDataDisplayComponent, + ], + templateUrl: './view-lib.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ViewLibComponent { + protected readonly quoteStore = inject(QuoteStore); + + protected readonly state = this.quoteStore.quoteState(); +} diff --git a/frontend/libs/view-lib/src/test-setup.ts b/frontend/libs/view-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/view-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/view-lib/tsconfig.json b/frontend/libs/view-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/view-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/view-lib/tsconfig.lib.json b/frontend/libs/view-lib/tsconfig.lib.json new file mode 100644 index 0000000..64216c9 --- /dev/null +++ b/frontend/libs/view-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/view-lib/tsconfig.spec.json b/frontend/libs/view-lib/tsconfig.spec.json new file mode 100644 index 0000000..aad9b06 --- /dev/null +++ b/frontend/libs/view-lib/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": [ + "src/test-setup.ts" + ], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 6e6ff3f..6c9434f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,7 +18,8 @@ "@target/input-lib": ["frontend/libs/input-lib/src/index.ts"], "@target/interfaces": ["shared/interfaces/src/index.ts"], "@target/ui-lib": ["frontend/libs/ui-lib/src/index.ts"], - "@target/validations": ["shared/validations/src/index.ts"] + "@target/validations": ["shared/validations/src/index.ts"], + "@target/view-lib": ["frontend/libs/view-lib/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] From bfdfafa7303076a753753935b604f56c55518afa Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Fri, 4 Apr 2025 12:20:26 +0200 Subject: [PATCH 16/26] =?UTF-8?q?=E2=9C=A8=20Quote=20Summary=20Page:=20ext?= =?UTF-8?q?end=20quote=20summary=20for=20data=20fetching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/input-lib/store/services/quote.service.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts b/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts index 103eaf3..ceeb540 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts @@ -1,6 +1,10 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; +import { + QuoteCreateResponseDto, + QuoteRequestDto, + QuoteResponseDto, +} from '@target/interfaces'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) @@ -12,4 +16,10 @@ export class QuoteService { ): Observable { return this.http.post('/api/quote', quoteDto); } + + fetchQuote(quoteId: string): Observable { + return this.http.get('/api/quote', { + params: { quoteId }, + }); + } } From 815227038a7daebdd90a7858748414279959cfe3 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Fri, 4 Apr 2025 12:20:48 +0200 Subject: [PATCH 17/26] =?UTF-8?q?=E2=9C=A8=20Quote=20Summary=20Page:=20rem?= =?UTF-8?q?ove=20form=20submission=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/input-lib/input-lib.component.html | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html index 7143655..d38333e 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html @@ -126,20 +126,20 @@
- @if (inputStore.uiState().quote.beitrag.einmalbeitrag || inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente) { -
-
- - Einmalbeitrag: {{ inputStore.uiState().quote.beitrag.einmalbeitrag | currency : 'EUR' }} - - - Garantierte Mindestrente: {{ inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente | currency : 'EUR' }} - -
-
- } + + + + + + + + + + + + + +
From b5e35be202604b0ee8dc10f54b6112316c4cc408 Mon Sep 17 00:00:00 2001 From: "julio.fernandez" Date: Mon, 7 Apr 2025 16:30:27 +0200 Subject: [PATCH 18/26] =?UTF-8?q?=E2=9C=A8=20Quote=20Summary=20Page:=20Lon?= =?UTF-8?q?g=20String=20Generator:=20add=20service=20and=20unit=20tests=20?= =?UTF-8?q?for=20string=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/__tests__/app.controller.spec.ts | 65 -------- backend/apps/bff/src/app/app.controller.ts | 16 -- backend/apps/bff/src/app/app.module.ts | 7 +- .../quote/__tests__/quote.service.spec.ts | 49 ------ .../src/app/services/quote/quote.service.ts | 36 ----- backend/apps/bff/src/common/common.module.ts | 10 ++ .../long-string-generator.service.spec.ts | 21 +++ .../long-string-generator.service.ts | 9 ++ .../services/timing/timing.service.spec.ts | 18 +++ .../common/services/timing/timing.service.ts | 5 + .../__tests__/quote.controller.spec.ts | 91 ++++++++++++ .../src/quote/controllers/quoteController.ts | 29 ++++ .../quote.repository.service.spec.ts | 78 ++++++++++ .../src/quote/dao/quote/quote.repository.ts | 17 +++ .../bff/src/quote/data/mock/quote.mock.ts | 19 +++ .../bff/src/quote/data/schema/quote.schema.ts | 5 + .../{app => quote}/pipes/validation.pipe.ts | 0 backend/apps/bff/src/quote/quote.module.ts | 14 ++ .../quote/__tests__/quote.service.spec.ts | 139 ++++++++++++++++++ .../src/quote/services/quote/quote.service.ts | 54 +++++++ .../quote/store/__tests__/quote.store.spec.ts | 60 ++++++++ .../apps/bff/src/quote/store/quote.store.ts | 23 +++ 22 files changed, 594 insertions(+), 171 deletions(-) delete mode 100644 backend/apps/bff/src/app/__tests__/app.controller.spec.ts delete mode 100644 backend/apps/bff/src/app/app.controller.ts delete mode 100644 backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts delete mode 100644 backend/apps/bff/src/app/services/quote/quote.service.ts create mode 100644 backend/apps/bff/src/common/common.module.ts create mode 100644 backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.spec.ts create mode 100644 backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.ts create mode 100644 backend/apps/bff/src/common/services/timing/timing.service.spec.ts create mode 100644 backend/apps/bff/src/common/services/timing/timing.service.ts create mode 100644 backend/apps/bff/src/quote/controllers/__tests__/quote.controller.spec.ts create mode 100644 backend/apps/bff/src/quote/controllers/quoteController.ts create mode 100644 backend/apps/bff/src/quote/dao/quote/__tests__/quote.repository.service.spec.ts create mode 100644 backend/apps/bff/src/quote/dao/quote/quote.repository.ts create mode 100644 backend/apps/bff/src/quote/data/mock/quote.mock.ts create mode 100644 backend/apps/bff/src/quote/data/schema/quote.schema.ts rename backend/apps/bff/src/{app => quote}/pipes/validation.pipe.ts (100%) create mode 100644 backend/apps/bff/src/quote/quote.module.ts create mode 100644 backend/apps/bff/src/quote/services/quote/__tests__/quote.service.spec.ts create mode 100644 backend/apps/bff/src/quote/services/quote/quote.service.ts create mode 100644 backend/apps/bff/src/quote/store/__tests__/quote.store.spec.ts create mode 100644 backend/apps/bff/src/quote/store/quote.store.ts diff --git a/backend/apps/bff/src/app/__tests__/app.controller.spec.ts b/backend/apps/bff/src/app/__tests__/app.controller.spec.ts deleted file mode 100644 index a989766..0000000 --- a/backend/apps/bff/src/app/__tests__/app.controller.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { QuoteRequestDto } from '@target/interfaces'; - -import { AppController } from '../app.controller'; -import { QuoteService } from '../services/quote/quote.service'; - -jest.mock('../services/quote/quote.service'); - -describe('AppController', () => { - let appController: AppController; - let quoteService: jest.Mocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [QuoteService], - }).compile(); - - appController = module.get(AppController); - quoteService = module.get(QuoteService); - }); - - describe('getQuote', () => { - it('should call process quote with a valid birthday', async () => { - const geburtsdatum = '2020-01-01'; - - const mockQuoteDto: QuoteRequestDto = { - geburtsdatum, - leistungsVorgabe: 'Beitrag', - beitrag: 1000, - berechnungDerLaufzeit: 'Alter bei Rentenbeginn', - laufzeit: 10, - beitragszahlungsweise: 'Einmalbeitrag', - rentenzahlungsweise: 'Monatliche Renten', - } as QuoteRequestDto; - - const expectedResult = { - basisdaten: { - geburtsdatum, - versicherungsbeginn: '2024-01-01', - garantieniveau: '100%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 30, - }, - leistungsmerkmale: { - garantierteMindestrente: 1000, - einmaligesGarantiekapital: 50000, - todesfallleistungAbAltersrentenbezug: 40000, - }, - beitrag: { - einmalbeitrag: 50000, - beitragsdynamik: '3%', - }, - }; - - quoteService.getQuote.mockResolvedValue(expectedResult); - - const result = await appController.getQuote(mockQuoteDto); - - expect(quoteService.getQuote).toHaveBeenCalledWith(mockQuoteDto); - expect(result).toBe(expectedResult); - }); - }); -}); diff --git a/backend/apps/bff/src/app/app.controller.ts b/backend/apps/bff/src/app/app.controller.ts deleted file mode 100644 index 5195446..0000000 --- a/backend/apps/bff/src/app/app.controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; -import { InputDtoSchema } from '@target/validations'; - -import { ValidationPipe } from './pipes/validation.pipe'; -import { QuoteService } from './services/quote/quote.service'; - -@Controller() -export class AppController { - constructor(private readonly quoteService: QuoteService) {} - - @Post('/quote') - getQuote(@Body(new ValidationPipe(InputDtoSchema)) quoteDto: QuoteRequestDto): Promise { - return this.quoteService.getQuote(quoteDto); - } -} diff --git a/backend/apps/bff/src/app/app.module.ts b/backend/apps/bff/src/app/app.module.ts index 4de312e..d9db422 100644 --- a/backend/apps/bff/src/app/app.module.ts +++ b/backend/apps/bff/src/app/app.module.ts @@ -1,11 +1,8 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { QuoteService } from './services/quote/quote.service'; +import { QuoteModule } from '../quote/quote.module'; @Module({ - imports: [], - controllers: [AppController], - providers: [QuoteService], + imports: [QuoteModule], }) export class AppModule {} diff --git a/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts b/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts deleted file mode 100644 index 93854f0..0000000 --- a/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Test } from '@nestjs/testing'; - -import { QuoteService } from '../quote.service'; - -describe('QuoteService', () => { - let service: QuoteService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - providers: [QuoteService], - }).compile(); - - service = app.get(QuoteService); - }); - - describe('getQuote', () => { - it('should return quote details for given contribution amount', async () => { - const geburtsdatum = '2000-01-01'; - - const beitrag = 1000; - - const expectedQuote = { - basisdaten: { - geburtsdatum: geburtsdatum, - versicherungsbeginn: '2025-02-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 10, - }, - leistungsmerkmale: { - garantierteMindestrente: beitrag * 50, - einmaligesGarantiekapital: beitrag / 2, - todesfallleistungAbAltersrentenbezug: 67, - }, - beitrag: { - einmalbeitrag: beitrag, - beitragsdynamik: '1,5%', - }, - }; - const result = await service.getQuote({ - beitrag, - geburtsdatum, - }); - - expect(result).toEqual(expectedQuote); - }); - }); -}); diff --git a/backend/apps/bff/src/app/services/quote/quote.service.ts b/backend/apps/bff/src/app/services/quote/quote.service.ts deleted file mode 100644 index 5f054b0..0000000 --- a/backend/apps/bff/src/app/services/quote/quote.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; - -@Injectable() -export class QuoteService { - async getQuote({ - beitrag, - geburtsdatum, - }: QuoteRequestDto): Promise { - await this.sleep(Math.random() * 4000); // Simulate a real quote service delay 😅 - - return { - basisdaten: { - geburtsdatum: geburtsdatum, - versicherungsbeginn: '2025-02-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 10, - }, - leistungsmerkmale: { - garantierteMindestrente: beitrag * 50, - einmaligesGarantiekapital: beitrag / 2, - todesfallleistungAbAltersrentenbezug: 67, - }, - beitrag: { - einmalbeitrag: beitrag, - beitragsdynamik: '1,5%', - }, - }; - } - - private async sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } -} diff --git a/backend/apps/bff/src/common/common.module.ts b/backend/apps/bff/src/common/common.module.ts new file mode 100644 index 0000000..183bf4f --- /dev/null +++ b/backend/apps/bff/src/common/common.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { LongStringGeneratorService } from './services/long-string-generator/long-string-generator.service'; +import { TimingService } from './services/timing/timing.service'; + +@Module({ + providers: [LongStringGeneratorService, TimingService], + exports: [LongStringGeneratorService, TimingService], +}) +export class CommonModule {} diff --git a/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.spec.ts b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.spec.ts new file mode 100644 index 0000000..2238ca8 --- /dev/null +++ b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.spec.ts @@ -0,0 +1,21 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { LongStringGeneratorService } from './long-string-generator.service'; + +describe('LongStringGeneratorService', () => { + let service: LongStringGeneratorService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LongStringGeneratorService], + }).compile(); + + service = module.get( + LongStringGeneratorService + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.ts b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.ts new file mode 100644 index 0000000..3b27e4f --- /dev/null +++ b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { randomBytes } from 'crypto'; + +@Injectable() +export class LongStringGeneratorService { + generate(size = 10) { + return randomBytes(size).toString('hex'); + } +} diff --git a/backend/apps/bff/src/common/services/timing/timing.service.spec.ts b/backend/apps/bff/src/common/services/timing/timing.service.spec.ts new file mode 100644 index 0000000..d6c8acb --- /dev/null +++ b/backend/apps/bff/src/common/services/timing/timing.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TimingService } from './timing.service'; + +describe('TimingService', () => { + let service: TimingService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [TimingService], + }).compile(); + + service = module.get(TimingService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/apps/bff/src/common/services/timing/timing.service.ts b/backend/apps/bff/src/common/services/timing/timing.service.ts new file mode 100644 index 0000000..5c66b5e --- /dev/null +++ b/backend/apps/bff/src/common/services/timing/timing.service.ts @@ -0,0 +1,5 @@ +export class TimingService { + static async sleep(ms: number = 4000): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/backend/apps/bff/src/quote/controllers/__tests__/quote.controller.spec.ts b/backend/apps/bff/src/quote/controllers/__tests__/quote.controller.spec.ts new file mode 100644 index 0000000..4e02c85 --- /dev/null +++ b/backend/apps/bff/src/quote/controllers/__tests__/quote.controller.spec.ts @@ -0,0 +1,91 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test, TestingModule } from '@nestjs/testing'; +import { QuoteRequestDto } from '@target/interfaces'; + +import { QuoteSchema } from '../../data/schema/quote.schema'; +import { QuoteService } from '../../services/quote/quote.service'; +import { QuoteController } from '../quoteController'; + +describe('QuotaController', () => { + let appController: QuoteController; + let quoteService: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [QuoteController], + providers: [ + { + provide: QuoteService, + useValue: { calculateQuote: jest.fn(), createQuote: jest.fn() }, + }, + ], + }).compile(); + + appController = module.get(QuoteController); + quoteService = module.get(QuoteService); + }); + + it('should be defined', () => { + expect(appController).toBeDefined(); + expect(quoteService).toBeDefined(); + }); + + describe('getQuote', () => { + it('should call process quote with a valid birthday', async () => { + const mockQuoteDto: QuoteRequestDto = { + geburtsdatum: '2020-01-01', + leistungsVorgabe: 'Beitrag', + beitrag: 1000, + berechnungDerLaufzeit: 'Alter bei Rentenbeginn', + laufzeit: 10, + beitragszahlungsweise: 'Einmalbeitrag', + rentenzahlungsweise: 'Monatliche Renten', + }; + + const calculatedQuote: Omit = { + basisdaten: { + geburtsdatum: '2020-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '100%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 30, + }, + leistungsmerkmale: { + garantierteMindestrente: 1000, + einmaligesGarantiekapital: 50000, + todesfallleistungAbAltersrentenbezug: 40000, + }, + beitrag: { + einmalbeitrag: 50000, + beitragsdynamik: '3%', + }, + }; + + const expectedQuote = { + ...calculatedQuote, + id: fakerEN.string.alpha({ length: 15 }), + }; + + jest + .spyOn(quoteService, 'calculateQuote') + .mockReturnValueOnce(calculatedQuote); + jest + .spyOn(quoteService, 'createQuote') + .mockResolvedValueOnce(expectedQuote); + + const result = await appController.post(mockQuoteDto); + + expect(quoteService.calculateQuote).toHaveBeenCalledWith({ + beitrag: mockQuoteDto.beitrag, + geburtsdatum: mockQuoteDto.geburtsdatum, + }); + expect(quoteService.calculateQuote).toHaveBeenCalledTimes(1); + + expect(quoteService.createQuote).toHaveBeenCalledWith(calculatedQuote); + expect(quoteService.createQuote).toHaveBeenCalledTimes(1); + + expect(result).toBe(expectedQuote); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/controllers/quoteController.ts b/backend/apps/bff/src/quote/controllers/quoteController.ts new file mode 100644 index 0000000..4a49e0e --- /dev/null +++ b/backend/apps/bff/src/quote/controllers/quoteController.ts @@ -0,0 +1,29 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; +import { InputDtoSchema } from '@target/validations'; + +import { QuoteSchema } from '../data/schema/quote.schema'; +import { ValidationPipe } from '../pipes/validation.pipe'; +import { QuoteService } from '../services/quote/quote.service'; + +@Controller('/quote') +export class QuoteController { + constructor(private readonly quoteService: QuoteService) {} + + @Post() + async post( + @Body(new ValidationPipe(InputDtoSchema)) quoteDto: QuoteRequestDto + ): Promise { + const quote = this.quoteService.calculateQuote({ + beitrag: quoteDto.beitrag, + geburtsdatum: quoteDto.geburtsdatum, + }); + + return await this.quoteService.createQuote(quote); + } + + @Get('/:id') + async get(@Param('id') id: string): Promise { + return await this.quoteService.findOnById(id); + } +} diff --git a/backend/apps/bff/src/quote/dao/quote/__tests__/quote.repository.service.spec.ts b/backend/apps/bff/src/quote/dao/quote/__tests__/quote.repository.service.spec.ts new file mode 100644 index 0000000..f858547 --- /dev/null +++ b/backend/apps/bff/src/quote/dao/quote/__tests__/quote.repository.service.spec.ts @@ -0,0 +1,78 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { QuoteSchema } from '../../../data/schema/quote.schema'; +import { QuoteStore } from '../../../store/quote.store'; +import { QuoteRepository } from '../quote.repository'; + +describe('QuoteRepositoryService', () => { + let service: QuoteRepository; + let quoteStore: QuoteStore; + + const entityMock: QuoteSchema = { + id: fakerEN.string.alpha({ length: 15 }), + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 1000 * 50, + einmaligesGarantiekapital: 1000 / 2, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuoteRepository, + { + provide: QuoteStore, + useValue: { insert: jest.fn(), retrieve: jest.fn() }, + }, + ], + }).compile(); + + service = module.get(QuoteRepository); + quoteStore = module.get(QuoteStore); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + expect(quoteStore).toBeDefined(); + }); + + describe('create', () => { + it('should create quote', async () => { + jest.spyOn(quoteStore, 'insert').mockResolvedValueOnce(entityMock); + + const res = await service.create(entityMock); + + expect(quoteStore.insert).toHaveBeenCalledWith(entityMock); + expect(quoteStore.insert).toHaveBeenCalledTimes(1); + + expect(res).toEqual(entityMock); + }); + }); + + describe('retrieve', () => { + it('should retrieve quote', async () => { + jest.spyOn(quoteStore, 'retrieve').mockResolvedValueOnce(entityMock); + + const res = await service.findOneById(entityMock.id); + + expect(quoteStore.retrieve).toHaveBeenCalledWith(entityMock.id); + expect(quoteStore.retrieve).toHaveBeenCalledTimes(1); + + expect(res).toEqual(entityMock); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/dao/quote/quote.repository.ts b/backend/apps/bff/src/quote/dao/quote/quote.repository.ts new file mode 100644 index 0000000..ec9bc8e --- /dev/null +++ b/backend/apps/bff/src/quote/dao/quote/quote.repository.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; + +import { QuoteSchema } from '../../data/schema/quote.schema'; +import { QuoteStore } from '../../store/quote.store'; + +@Injectable() +export class QuoteRepository { + constructor(private quotaStore: QuoteStore) {} + + async create(entity: QuoteSchema): Promise { + return await this.quotaStore.insert(entity); + } + + async findOneById(id: string): Promise { + return await this.quotaStore.retrieve(id); + } +} diff --git a/backend/apps/bff/src/quote/data/mock/quote.mock.ts b/backend/apps/bff/src/quote/data/mock/quote.mock.ts new file mode 100644 index 0000000..cb2af12 --- /dev/null +++ b/backend/apps/bff/src/quote/data/mock/quote.mock.ts @@ -0,0 +1,19 @@ +export const quoteMock = { + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 1000 * 50, + einmaligesGarantiekapital: 1000 / 2, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, +}; diff --git a/backend/apps/bff/src/quote/data/schema/quote.schema.ts b/backend/apps/bff/src/quote/data/schema/quote.schema.ts new file mode 100644 index 0000000..9a6a77d --- /dev/null +++ b/backend/apps/bff/src/quote/data/schema/quote.schema.ts @@ -0,0 +1,5 @@ +import { QuoteResponseDto } from '@target/interfaces'; + +export interface QuoteSchema extends QuoteResponseDto { + id?: string; +} diff --git a/backend/apps/bff/src/app/pipes/validation.pipe.ts b/backend/apps/bff/src/quote/pipes/validation.pipe.ts similarity index 100% rename from backend/apps/bff/src/app/pipes/validation.pipe.ts rename to backend/apps/bff/src/quote/pipes/validation.pipe.ts diff --git a/backend/apps/bff/src/quote/quote.module.ts b/backend/apps/bff/src/quote/quote.module.ts new file mode 100644 index 0000000..dd3d57f --- /dev/null +++ b/backend/apps/bff/src/quote/quote.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { CommonModule } from '../common/common.module'; +import { QuoteController } from './controllers/quoteController'; +import { QuoteRepository } from './dao/quote/quote.repository'; +import { QuoteService } from './services/quote/quote.service'; +import { QuoteStore } from './store/quote.store'; + +@Module({ + imports: [CommonModule], + controllers: [QuoteController], + providers: [QuoteRepository, QuoteService, QuoteStore], +}) +export class QuoteModule {} diff --git a/backend/apps/bff/src/quote/services/quote/__tests__/quote.service.spec.ts b/backend/apps/bff/src/quote/services/quote/__tests__/quote.service.spec.ts new file mode 100644 index 0000000..b3ebe22 --- /dev/null +++ b/backend/apps/bff/src/quote/services/quote/__tests__/quote.service.spec.ts @@ -0,0 +1,139 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test } from '@nestjs/testing'; + +import { LongStringGeneratorService } from '../../../../common/services/long-string-generator/long-string-generator.service'; +import { QuoteRepository } from '../../../dao/quote/quote.repository'; +import { QuoteService } from '../quote.service'; + +describe('QuoteService', () => { + let service: QuoteService; + let quoteRepository: QuoteRepository; + let longStringGeneratorService: LongStringGeneratorService; + + const entityMock = { + basisdaten: { + geburtsdatum: '2000-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 5000, + einmaligesGarantiekapital: 500, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(async () => { + const app = await Test.createTestingModule({ + providers: [ + QuoteService, + { + provide: QuoteRepository, + useValue: { create: jest.fn(), findOneById: jest.fn() }, + }, + { + provide: LongStringGeneratorService, + useValue: { generate: jest.fn() }, + }, + ], + }).compile(); + + service = app.get(QuoteService); + quoteRepository = app.get(QuoteRepository); + longStringGeneratorService = app.get( + LongStringGeneratorService + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + expect(quoteRepository).toBeDefined(); + expect(longStringGeneratorService).toBeDefined(); + }); + + describe('findOnById', () => { + it('should return quote details for given quote id', async () => { + const id = fakerEN.string.alpha({ length: 15 }); + + const expectedQuote = { + id, + ...entityMock, + }; + + jest + .spyOn(quoteRepository, 'findOneById') + .mockResolvedValue(expectedQuote); + + const result = await service.findOnById(id); + + expect(quoteRepository.findOneById).toHaveBeenCalledWith(id); + expect(quoteRepository.findOneById).toHaveBeenCalledTimes(1); + + expect(result).toEqual(expectedQuote); + }); + }); + + describe('createQuote', () => { + it('should save quote details from a quote schema', async () => { + const id = fakerEN.string.alpha({ length: 15 }); + + const expectedQuote = { + id, + ...entityMock, + }; + + jest.spyOn(quoteRepository, 'create').mockResolvedValue(expectedQuote); + jest.spyOn(longStringGeneratorService, 'generate').mockReturnValue(id); + + const result = await service.createQuote(entityMock); + + expect(quoteRepository.create).toHaveBeenCalledWith(expectedQuote); + expect(quoteRepository.create).toHaveBeenCalledTimes(1); + + expect(longStringGeneratorService.generate).toHaveBeenCalledTimes(1); + + expect(result).toEqual(expectedQuote); + }); + }); + + describe('calculateQuote', () => { + it('should calculate quote from initial params', async () => { + const geburtsdatum = '2000-01-01'; + const beitrag = 1000; + + const entityMock = { + basisdaten: { + geburtsdatum: geburtsdatum, + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: beitrag * 50, + einmaligesGarantiekapital: beitrag / 2, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: beitrag, + beitragsdynamik: '1,5%', + }, + }; + + const result = service.calculateQuote({ + beitrag, + geburtsdatum, + }); + + expect(result).toEqual(entityMock); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/services/quote/quote.service.ts b/backend/apps/bff/src/quote/services/quote/quote.service.ts new file mode 100644 index 0000000..2f12c86 --- /dev/null +++ b/backend/apps/bff/src/quote/services/quote/quote.service.ts @@ -0,0 +1,54 @@ +import { HttpException, Injectable, NotFoundException } from '@nestjs/common'; +import { QuoteRequestDto } from '@target/interfaces'; + +import { LongStringGeneratorService } from '../../../common/services/long-string-generator/long-string-generator.service'; +import { QuoteRepository } from '../../dao/quote/quote.repository'; +import { quoteMock } from '../../data/mock/quote.mock'; +import { QuoteSchema } from '../../data/schema/quote.schema'; + +@Injectable() +export class QuoteService { + constructor( + private quoteRepository: QuoteRepository, + private longStringGeneratorService: LongStringGeneratorService + ) {} + + calculateQuote({ + beitrag, + geburtsdatum, + }: QuoteRequestDto): Omit { + return { + basisdaten: { ...quoteMock.basisdaten, geburtsdatum: geburtsdatum }, + leistungsmerkmale: { + ...quoteMock.leistungsmerkmale, + garantierteMindestrente: beitrag * 50, + einmaligesGarantiekapital: beitrag / 2, + }, + beitrag: { + ...quoteMock.beitrag, + einmalbeitrag: beitrag, + }, + } as Omit; + } + + async createQuote(quote: Omit): Promise { + try { + const _quote = { + ...quote, + id: this.longStringGeneratorService.generate(), + }; + + return await this.quoteRepository.create(_quote); + } catch (entityInsertError) { + throw new HttpException(entityInsertError.message, entityInsertError); + } + } + + async findOnById(id: string): Promise { + try { + return await this.quoteRepository.findOneById(id); + } catch (entityFindError) { + throw new NotFoundException(entityFindError.message); + } + } +} diff --git a/backend/apps/bff/src/quote/store/__tests__/quote.store.spec.ts b/backend/apps/bff/src/quote/store/__tests__/quote.store.spec.ts new file mode 100644 index 0000000..a56e79b --- /dev/null +++ b/backend/apps/bff/src/quote/store/__tests__/quote.store.spec.ts @@ -0,0 +1,60 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { QuoteSchema } from '../../data/schema/quote.schema'; +import { QuoteStore } from '../quote.store'; + +describe('QuoteStore', () => { + let service: QuoteStore; + + const entityMock: QuoteSchema = { + id: fakerEN.string.alpha({ length: 15 }), + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 50000, + einmaligesGarantiekapital: 500, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [QuoteStore], + }).compile(); + + service = module.get(QuoteStore); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('insert', () => { + it('should create quote', async () => { + const res = await service.insert(entityMock); + + expect(res).toEqual(entityMock); + }); + }); + + describe('retrieve', () => { + it('should get quote', async () => { + service['quotaStorage'].set(entityMock.id, entityMock); + + const res = await service.retrieve(entityMock.id); + + expect(res).toEqual(entityMock); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/store/quote.store.ts b/backend/apps/bff/src/quote/store/quote.store.ts new file mode 100644 index 0000000..ed0658d --- /dev/null +++ b/backend/apps/bff/src/quote/store/quote.store.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +import { TimingService } from '../../common/services/timing/timing.service'; +import { QuoteSchema } from '../data/schema/quote.schema'; + +@Injectable() +export class QuoteStore { + private quotaStorage = new Map(); + + async insert(entity: QuoteSchema): Promise { + const _entity = this.quotaStorage.set(entity.id, entity); + + await TimingService.sleep(); + + return _entity.get(entity.id); + } + + async retrieve(id: string): Promise { + await TimingService.sleep(); + + return this.quotaStorage.get(id); + } +} From 2b31d9d43fe0e65644242568d9d2c113fde533a6 Mon Sep 17 00:00:00 2001 From: "julio.fernandez" Date: Mon, 7 Apr 2025 17:19:19 +0200 Subject: [PATCH 19/26] Quote Summary Page:Implement quote view flow --- frontend/apps/shell/src/app/app.routes.ts | 5 +- .../__tests__/input-lib.component.spec.ts | 121 ++++++++++-------- .../src/lib/input-lib/input-lib.component.ts | 7 +- .../src/lib/input-lib/store/input.store.ts | 12 +- .../input-lib/store/services/quote.service.ts | 4 +- 5 files changed, 87 insertions(+), 62 deletions(-) diff --git a/frontend/apps/shell/src/app/app.routes.ts b/frontend/apps/shell/src/app/app.routes.ts index 9526e55..a200b81 100644 --- a/frontend/apps/shell/src/app/app.routes.ts +++ b/frontend/apps/shell/src/app/app.routes.ts @@ -2,9 +2,11 @@ import { Route } from '@angular/router'; import { InputLibComponent } from '@target/input-lib'; import { ViewLibComponent } from '@target/view-lib'; +import { quoteViewResolver } from './resolvers/quote-view.resolver'; + const ROUTES = { INPUTS: 'inputs', - VIEW: 'view', + VIEW: 'view/:id', }; export const appRoutes: Route[] = [ @@ -20,6 +22,7 @@ export const appRoutes: Route[] = [ { path: ROUTES.VIEW, component: ViewLibComponent, + resolve: [quoteViewResolver], }, { path: '**', diff --git a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts index ffb1e64..8822609 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts @@ -19,70 +19,87 @@ describe('InputLibComponent', () => { beforeEach(async () => { const mockHttp = { - post: jest.fn() + post: jest.fn(), } as Partial; const mockQuoteService = { - calculateQuote: jest.fn().mockReturnValue(of({ - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2024-01-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 30 - }, - leistungsmerkmale: { - garantierteMindestrente: 1000, - einmaligesGarantiekapital: 100000, - todesfallleistungAbAltersrentenbezug: 50000 - }, - beitrag: { - einmalbeitrag: 50000, - beitragsdynamik: '3%' - } - })), - http: mockHttp as HttpClient + calculateQuote: jest.fn().mockReturnValue( + of({ + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 30, + }, + leistungsmerkmale: { + garantierteMindestrente: 1000, + einmaligesGarantiekapital: 100000, + todesfallleistungAbAltersrentenbezug: 50000, + }, + beitrag: { + einmalbeitrag: 50000, + beitragsdynamik: '3%', + }, + }) + ), + http: mockHttp as HttpClient, }; const mockState: InputState = { geburtsdatum: { value: '2002-02-04', valid: true, error: null }, leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, beitrag: { value: 1000, valid: true, error: null }, - berechnungDerLaufzeit: { value: 'Alter bei Rentenbeginn', valid: true, error: null }, + berechnungDerLaufzeit: { + value: 'Alter bei Rentenbeginn', + valid: true, + error: null, + }, laufzeit: { value: 10, valid: true, error: null }, - beitragszahlungsweise: { value: 'Einmalbeitrag', valid: true, error: null }, - rentenzahlungsweise: { value: 'Monatliche Renten', valid: true, error: null }, - quote: { - basisdaten: { - geburtsdatum: '', - versicherungsbeginn: '', - garantieniveau: '', - alterBeiRentenbeginn: 0, - aufschubdauer: 0, - beitragszahlungsdauer: 0 - }, - leistungsmerkmale: { - garantierteMindestrente: 0, - einmaligesGarantiekapital: 0, - todesfallleistungAbAltersrentenbezug: 0 - }, - beitrag: { - einmalbeitrag: 0, - beitragsdynamik: '' - }, + beitragszahlungsweise: { + value: 'Einmalbeitrag', + valid: true, + error: null, }, + rentenzahlungsweise: { + value: 'Monatliche Renten', + valid: true, + error: null, + }, + // quote: { + // basisdaten: { + // geburtsdatum: '', + // versicherungsbeginn: '', + // garantieniveau: '', + // alterBeiRentenbeginn: 0, + // aufschubdauer: 0, + // beitragszahlungsdauer: 0 + // }, + // leistungsmerkmale: { + // garantierteMindestrente: 0, + // einmaligesGarantiekapital: 0, + // todesfallleistungAbAltersrentenbezug: 0 + // }, + // beitrag: { + // einmalbeitrag: 0, + // beitragsdynamik: '' + // }, + // }, }; await TestBed.configureTestingModule({ - imports: [InputLibComponent,NxSpinnerComponent], + imports: [InputLibComponent, NxSpinnerComponent], providers: [ provideHttpClient(), { provide: QuoteService, useValue: mockQuoteService }, - { provide: InputStore, useValue: { - updateInputs: jest.fn(), - calculate: jest.fn(), - uiState: jest.fn(() => mockState) - }} - ] + { + provide: InputStore, + useValue: { + updateInputs: jest.fn(), + calculate: jest.fn(), + uiState: jest.fn(() => mockState), + }, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(InputLibComponent); @@ -99,7 +116,7 @@ describe('InputLibComponent', () => { it('should update inputs through the store', async () => { const input: Input = { key: 'beitrag', - value: 2000 + value: 2000, }; await component.updateInputs(input); @@ -116,7 +133,7 @@ describe('InputLibComponent', () => { it('should handle string inputs', async () => { const input: Input = { key: 'leistungsVorgabe', - value: 'Rente' + value: 'Rente', }; await component.updateInputs(input); @@ -127,7 +144,7 @@ describe('InputLibComponent', () => { it('should handle numeric inputs', async () => { const input: Input = { key: 'laufzeit', - value: 15 + value: 15, }; await component.updateInputs(input); diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index f0e68c9..c466639 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { Component, inject, signal } from '@angular/core'; +import { Router } from '@angular/router'; import { NxErrorModule } from '@aposin/ng-aquila/base'; import { NxButtonModule } from '@aposin/ng-aquila/button'; import { @@ -43,6 +44,7 @@ import { Input } from './store/input.store.interfaces'; }) export class InputLibComponent { protected readonly inputStore = inject(InputStore); + private readonly router = inject(Router); protected isProcessingData = signal(false); protected errorResponse = signal({} as ApiErrorResponse); @@ -60,7 +62,10 @@ export class InputLibComponent { this.inputStore .calculate() - .then(() => this.isProcessingData.set(false)) + .then(async (res) => { + await this.router.navigate(['/view', res]); + this.isProcessingData.set(false); + }) .catch((err) => { if (err.status === 400) this.inputStore.processErrors(err.error.message); diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts index 6133f38..d753924 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts @@ -1,6 +1,6 @@ import { inject } from '@angular/core'; import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; -import { QuoteCreateResponseDto, QuoteRequestDto } from '@target/interfaces'; +import { QuoteRequestDto } from '@target/interfaces'; import { InputDtoSchema } from '@target/validations'; import { lastValueFrom } from 'rxjs'; @@ -78,7 +78,7 @@ export const InputStore = signalStore( }); }, - calculate: async (): Promise => { + calculate: async (): Promise => { const quoteDto = transformUiStateToInputDto(store.uiState()); const validationResult = await InputDtoSchema.safeParseAsync(quoteDto); @@ -116,9 +116,11 @@ export const InputStore = signalStore( throw new Error('Invalid input'); } - return lastValueFrom( - quoteService.calculateQuote(quoteDto as QuoteRequestDto) - ); + return ( + await lastValueFrom( + quoteService.calculateQuote(quoteDto as QuoteRequestDto) + ) + ).id; }, })) ); diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts b/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts index ceeb540..c596595 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts @@ -18,8 +18,6 @@ export class QuoteService { } fetchQuote(quoteId: string): Observable { - return this.http.get('/api/quote', { - params: { quoteId }, - }); + return this.http.get(`/api/quote/${quoteId}`); } } From f56afa7c43520a3e548996517ed6ca059a0dc704 Mon Sep 17 00:00:00 2001 From: "julio.fernandez" Date: Mon, 7 Apr 2025 17:19:27 +0200 Subject: [PATCH 20/26] Quote Summary Page:Implement quote view resolver --- .../quote-view-resolver.resolver.spec.ts | 19 +++++++++++++++++++ .../src/app/resolvers/quote-view.resolver.ts | 10 ++++++++++ 2 files changed, 29 insertions(+) create mode 100644 frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts create mode 100644 frontend/apps/shell/src/app/resolvers/quote-view.resolver.ts diff --git a/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts b/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts new file mode 100644 index 0000000..8cbe0df --- /dev/null +++ b/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; +import { ResolveFn } from '@angular/router'; + +import { quoteViewResolver } from '../quote-view.resolver'; + +describe('quoteViewResolverResolver', () => { + const executeResolver: ResolveFn = (...resolverParameters) => + TestBed.runInInjectionContext(() => + quoteViewResolver(...resolverParameters) + ); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeResolver).toBeTruthy(); + }); +}); diff --git a/frontend/apps/shell/src/app/resolvers/quote-view.resolver.ts b/frontend/apps/shell/src/app/resolvers/quote-view.resolver.ts new file mode 100644 index 0000000..f2bfb66 --- /dev/null +++ b/frontend/apps/shell/src/app/resolvers/quote-view.resolver.ts @@ -0,0 +1,10 @@ +import { inject } from '@angular/core'; +import { ResolveFn } from '@angular/router'; + +import { QuoteStore } from '../../../../../libs/view-lib/src/lib/view-lib/store/quote.store'; + +export const quoteViewResolver: ResolveFn = (route, state) => { + const quoteStore = inject(QuoteStore); + + return quoteStore.fetchQuote(route.params['id']); +}; From 94cb7a9b0264b4eef4f93b87423bf0473f3b3f7a Mon Sep 17 00:00:00 2001 From: "julio.fernandez" Date: Tue, 8 Apr 2025 16:06:27 +0200 Subject: [PATCH 21/26] Code Cleanup: input-lib refactoring --- .../lib/date-picker/datepicker.component.html | 4 +- .../lib/date-picker/datepicker.component.ts | 146 +++++++++++------- .../lib/input-lib/input-lib.component.html | 60 ++----- .../src/lib/input-lib/input-lib.component.ts | 69 +++++++-- .../src/lib/input-lib/store/input.store.ts | 17 +- .../src/lib/view-lib/view-lib.component.html | 22 +++ .../src/lib/input/beitragszahlungsweise.ts | 20 ++- .../src/lib/input/berechnung-der-laufzeit.ts | 20 ++- .../src/lib/input/leistungsvorgabe.ts | 17 +- .../src/lib/input/rentenzahlungsweise.ts | 26 +++- 10 files changed, 261 insertions(+), 140 deletions(-) diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html index 9629e30..ea5e024 100644 --- a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html +++ b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html @@ -5,8 +5,8 @@ nxDatefield nxInput [datepicker]="datePicker" - [value]="geburtsdatum()" - (dateInput)="updateDate($event)" + [(ngModel)]="value" + [disabled]="isDisabled" /> MM/DD/YYYY Please enter a valid date. diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts index 62a36a1..01d9500 100644 --- a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts +++ b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts @@ -1,59 +1,87 @@ -import { CommonModule } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - inject, - Input, - Output, - signal, -} from '@angular/core'; -import { NxErrorComponent } from '@aposin/ng-aquila/base'; -import { - NxDateAdapter, - NxDatefieldDirective, - NxDatepickerComponent, - NxDatepickerInputEvent, - NxDatepickerToggleComponent, -} from '@aposin/ng-aquila/datefield'; -import { NxFormfieldComponent } from '@aposin/ng-aquila/formfield'; -import { NxInputModule } from '@aposin/ng-aquila/input'; -import { NxMomentDateModule } from '@aposin/ng-aquila/moment-date-adapter'; -import moment from 'moment/moment'; - -@Component({ - selector: 'lib-date-picker', - standalone: true, - imports: [ - CommonModule, - NxInputModule, - NxMomentDateModule, - NxDatefieldDirective, - NxDatepickerComponent, - NxDatepickerToggleComponent, - NxErrorComponent, - NxFormfieldComponent, - ], - templateUrl: './datepicker.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DatepickerComponent { - // TODO: force input date format to DD-MM-YYYY - @Input() - set value(vle: string) { - this.geburtsdatum.set(this.adapter.parse(vle, 'YYYY-MM-DD', true)); - } - - @Output() dateChange: EventEmitter = new EventEmitter(); - - protected readonly adapter = inject(NxDateAdapter); - - protected geburtsdatum = signal(''); - - protected minDate = moment().subtract(100, 'years'); - protected maxDate = moment().subtract(18, 'years'); - - updateDate($event: NxDatepickerInputEvent): void { - this.dateChange.emit($event.target.value?.format('YYYY-MM-DD')); - } -} +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + forwardRef, + inject, +} from '@angular/core'; +import { + ControlValueAccessor, + FormsModule, + NG_VALUE_ACCESSOR, +} from '@angular/forms'; +import { NxErrorComponent } from '@aposin/ng-aquila/base'; +import { + NxDateAdapter, + NxDatefieldDirective, + NxDatepickerComponent, + NxDatepickerToggleComponent, +} from '@aposin/ng-aquila/datefield'; +import { NxFormfieldComponent } from '@aposin/ng-aquila/formfield'; +import { NxInputModule } from '@aposin/ng-aquila/input'; +import { NxMomentDateModule } from '@aposin/ng-aquila/moment-date-adapter'; +import moment, { Moment } from 'moment/moment'; + +@Component({ + selector: 'lib-date-picker', + standalone: true, + imports: [ + CommonModule, + NxInputModule, + NxMomentDateModule, + NxDatefieldDirective, + NxDatepickerComponent, + NxDatepickerToggleComponent, + NxErrorComponent, + NxFormfieldComponent, + FormsModule, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatepickerComponent), + multi: true, + }, + ], + templateUrl: './datepicker.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DatepickerComponent implements ControlValueAccessor { + protected isDisabled: boolean = false; + private _value: string = ''; + private onChange: any = () => {}; + private onTouched: any = () => {}; + + protected readonly adapter = inject(NxDateAdapter); + + protected minDate = moment().subtract(100, 'years'); + protected maxDate = moment().subtract(18, 'years'); + + get value(): string { + return this._value; + } + + set value(val: Moment) { + this._value = val?.format('YYYY-MM-DD'); + this.onChange(val?.format('YYYY-MM-DD')); + this.onTouched(); + } + + writeValue(value: string): void { + if (value !== undefined) { + this._value = ''; + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState?(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } +} diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html index d38333e..de52a0f 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html @@ -1,12 +1,11 @@ -
+
@@ -24,22 +23,13 @@
- - - Beitrag - Einmalbeitrag - Garantierte Mindestrente - Garantiekapital - Gesamtkapital - Gesamtrente - + +
- +
@@ -58,17 +48,12 @@
- - Alter bei Rentenbeginn - Aufschubdauer - +
- +
@@ -86,11 +71,7 @@
- - Einmalbeitrag - Monatliche Beiträge - +
@@ -98,13 +79,7 @@
- - Monatliche Renten - Vierteljährliche Renten - Halbjährliche Renten - Jährliche Renten - +
@@ -125,21 +100,6 @@
- - - - - - - - - - - - - - -
-
+ diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index c466639..cea8e45 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -1,12 +1,11 @@ import { CommonModule } from '@angular/common'; -import { Component, inject, signal } from '@angular/core'; +import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { NxErrorModule } from '@aposin/ng-aquila/base'; import { NxButtonModule } from '@aposin/ng-aquila/button'; -import { - NxDropdownComponent, - NxDropdownItemComponent, -} from '@aposin/ng-aquila/dropdown'; +import { NxDropdownComponent } from '@aposin/ng-aquila/dropdown'; import { NxFormfieldComponent } from '@aposin/ng-aquila/formfield'; import { NxColComponent, @@ -14,9 +13,17 @@ import { NxRowComponent, } from '@aposin/ng-aquila/grid'; import { NxInputModule } from '@aposin/ng-aquila/input'; +import { NxIsoDateModule } from '@aposin/ng-aquila/iso-date-adapter'; import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; import { ApiErrorResponse } from '@target/interfaces'; import { ErrorBoxComponent } from '@target/ui-lib'; +import { + beitragszahlungsweiseOpts, + berechnungDerLaufzeitOpts, + leistungsVorgabeOpts, + rentenzahlungsweiseOpts, +} from '@target/validations'; +import { debounceTime } from 'rxjs'; import { DatepickerComponent } from '../date-picker/datepicker.component'; import { InputStore } from './store/input.store'; @@ -31,30 +38,66 @@ import { Input } from './store/input.store.interfaces'; NxRowComponent, NxColComponent, NxFormfieldComponent, + NxIsoDateModule, NxDropdownComponent, - NxDropdownItemComponent, NxInputModule, NxErrorModule, NxButtonModule, - DatepickerComponent, NxSpinnerComponent, ErrorBoxComponent, + FormsModule, + ReactiveFormsModule, + DatepickerComponent, ], templateUrl: './input-lib.component.html', }) -export class InputLibComponent { +export class InputLibComponent implements OnInit { protected readonly inputStore = inject(InputStore); + + private readonly formBuilder = inject(FormBuilder); private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); protected isProcessingData = signal(false); protected errorResponse = signal({} as ApiErrorResponse); - updateInputs(input: Input): void { - this.isProcessingData.set(true); + protected readonly leistungsVorgabeOpts = leistungsVorgabeOpts; + protected readonly berechnungDerLaufzeitOpts = berechnungDerLaufzeitOpts; + protected readonly beitragszahlungsweiseOpts = beitragszahlungsweiseOpts; + protected readonly rentenzahlungsweiseOpts = rentenzahlungsweiseOpts; - this.inputStore - .updateInputs(input) - .then(() => this.isProcessingData.set(false)); + profile = this.formBuilder.group({ + geburtsdatum: [this.inputStore.uiState()['geburtsdatum'].value], + leistungsVorgabe: [this.inputStore.uiState()['leistungsVorgabe'].value], + beitrag: [this.inputStore.uiState()['beitrag'].value], + berechnungDerLaufzeit: [ + this.inputStore.uiState().berechnungDerLaufzeit.value, + ], + laufzeit: [this.inputStore.uiState().laufzeit.value], + beitragszahlungsweise: [ + this.inputStore.uiState().beitragszahlungsweise.value, + ], + rentenzahlungsweise: [this.inputStore.uiState().rentenzahlungsweise.value], + }); + + ngOnInit(): void { + this.profile.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef), debounceTime(100)) + .subscribe((values) => { + this.isProcessingData.set(true); + + const input: Input[] = Object.entries(values).map( + ([key, value]) => + ({ + key, + value, + } as Input) + ); + + this.inputStore + .updateInputs(input) + .then(() => this.isProcessingData.set(false)); + }); } calculate(): void { diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts index d753924..926187a 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts @@ -8,7 +8,7 @@ import { Input, InputState } from './input.store.interfaces'; import { QuoteService } from './services/quote.service'; const initialState: InputState = { - geburtsdatum: { value: '1985-07-25', valid: true, error: null }, + geburtsdatum: { value: '', valid: true, error: null }, leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, beitrag: { value: 1000, valid: true, error: null }, berechnungDerLaufzeit: { @@ -25,17 +25,19 @@ export const InputStore = signalStore( { providedIn: 'root' }, withState({ uiState: initialState }), withMethods((store, quoteService = inject(QuoteService)) => ({ - updateInputs: async (input: Input): Promise => { + updateInputs: async (input: Input[]): Promise => { const initialNewState = { ...store.uiState(), - [input.key]: { value: input.value, valid: true, error: null }, + ...transformFromInputArrayToInputState(input), }; + const validationResult = await InputDtoSchema.safeParseAsync( transformUiStateToInputDto(initialNewState) ); if (validationResult.success) { patchState(store, { uiState: initialNewState }); + return; } @@ -133,3 +135,12 @@ const transformUiStateToInputDto = (state: InputState): QuoteRequestDto => }), {} as QuoteRequestDto ); + +const transformFromInputArrayToInputState = (input: Input[]): {} => + input.reduce( + (acc, { key, value }) => ({ + ...acc, + [key]: { value: value, valid: true, error: null }, + }), + {} as InputState + ); diff --git a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html index 949cb64..b42f26c 100644 --- a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html +++ b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html @@ -1,6 +1,28 @@
+
+
+
+ + Einmalbeitrag: + {{ state.beitrag.einmalbeitrag | currency : 'EUR' }} + +
+
+
+
+ + Garantierte + Mindestrente: {{ state.leistungsmerkmale.garantierteMindestrente | currency : 'EUR' }} + +
+
+
+
+

Basisdaten

diff --git a/shared/validations/src/lib/input/beitragszahlungsweise.ts b/shared/validations/src/lib/input/beitragszahlungsweise.ts index 16fd71e..1eacd80 100644 --- a/shared/validations/src/lib/input/beitragszahlungsweise.ts +++ b/shared/validations/src/lib/input/beitragszahlungsweise.ts @@ -1,7 +1,21 @@ +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; import { z } from 'zod'; -export const BeitragszahlungsweiseEinmalbeitragSchema = z.literal('Einmalbeitrag'); -export const BeitragszahlungsweiseMonatlicheBeiträgeSchema = z.literal('Monatliche Beiträge'); +export const BeitragszahlungsweiseEinmalbeitragSchema = + z.literal('Einmalbeitrag'); +export const BeitragszahlungsweiseMonatlicheBeiträgeSchema = z.literal( + 'Monatliche Beiträge' +); -export const BeitragszahlungsweiseSchema = z.enum([BeitragszahlungsweiseEinmalbeitragSchema.value, BeitragszahlungsweiseMonatlicheBeiträgeSchema.value]); +export const BeitragszahlungsweiseSchema = z.enum([ + BeitragszahlungsweiseEinmalbeitragSchema.value, + BeitragszahlungsweiseMonatlicheBeiträgeSchema.value, +]); export type Beitragszahlungsweise = z.infer; + +export const beitragszahlungsweiseOpts: NxDropdownOption[] = Object.entries( + BeitragszahlungsweiseSchema.enum +).map(([label, value]) => ({ + label, + value, +})); diff --git a/shared/validations/src/lib/input/berechnung-der-laufzeit.ts b/shared/validations/src/lib/input/berechnung-der-laufzeit.ts index 78e3469..5a646c1 100644 --- a/shared/validations/src/lib/input/berechnung-der-laufzeit.ts +++ b/shared/validations/src/lib/input/berechnung-der-laufzeit.ts @@ -1,7 +1,21 @@ +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; import { z } from 'zod'; -export const BerechnungDerLaufzeitAlterBeiRentenbeginnSchema = z.literal('Alter bei Rentenbeginn'); -export const BerechnungDerLaufzeitAufschubdauerSchema = z.literal('Aufschubdauer'); +export const BerechnungDerLaufzeitAlterBeiRentenbeginnSchema = z.literal( + 'Alter bei Rentenbeginn' +); +export const BerechnungDerLaufzeitAufschubdauerSchema = + z.literal('Aufschubdauer'); -export const BerechnungDerLaufzeitSchema = z.enum([BerechnungDerLaufzeitAlterBeiRentenbeginnSchema.value, BerechnungDerLaufzeitAufschubdauerSchema.value]); +export const BerechnungDerLaufzeitSchema = z.enum([ + BerechnungDerLaufzeitAlterBeiRentenbeginnSchema.value, + BerechnungDerLaufzeitAufschubdauerSchema.value, +]); export type BerechnungDerLaufzeit = z.infer; + +export const berechnungDerLaufzeitOpts: NxDropdownOption[] = Object.entries( + BerechnungDerLaufzeitSchema.enum +).map(([label, value]) => ({ + label, + value, +})); diff --git a/shared/validations/src/lib/input/leistungsvorgabe.ts b/shared/validations/src/lib/input/leistungsvorgabe.ts index 0623f44..3044f2a 100644 --- a/shared/validations/src/lib/input/leistungsvorgabe.ts +++ b/shared/validations/src/lib/input/leistungsvorgabe.ts @@ -1,9 +1,13 @@ -import { z } from "zod"; +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; +import { z } from 'zod'; export const LeistungsvorgabeBeitragSchema = z.literal('Beitrag'); export const LeistungsvorgabeEinmalbeitragSchema = z.literal('Einmalbeitrag'); -export const LeistungsvorgabeGarantierteMindestrenteSchema = z.literal('Garantierte Mindestrente'); -export const LeistungsvorgabeGarantiekapitalSchema = z.literal('Garantiekapital'); +export const LeistungsvorgabeGarantierteMindestrenteSchema = z.literal( + 'Garantierte Mindestrente' +); +export const LeistungsvorgabeGarantiekapitalSchema = + z.literal('Garantiekapital'); export const LeistungsvorgabeGesamtkapitalSchema = z.literal('Gesamtkapital'); export const LeistungsvorgabeGesamtrenteSchema = z.literal('Gesamtrente'); @@ -16,3 +20,10 @@ export const LeistungsvorgabeSchema = z.enum([ LeistungsvorgabeGesamtrenteSchema.value, ]); export type Leistungsvorgabe = z.infer; + +export const leistungsVorgabeOpts: NxDropdownOption[] = Object.entries( + LeistungsvorgabeSchema.enum +).map(([label, value]) => ({ + label, + value, +})); diff --git a/shared/validations/src/lib/input/rentenzahlungsweise.ts b/shared/validations/src/lib/input/rentenzahlungsweise.ts index a6accae..97fcb02 100644 --- a/shared/validations/src/lib/input/rentenzahlungsweise.ts +++ b/shared/validations/src/lib/input/rentenzahlungsweise.ts @@ -1,9 +1,27 @@ +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; import { z } from 'zod'; -export const RentenzahlungsweiseMonatlichSchema = z.literal('Monatliche Renten'); -export const RentenzahlungsweiseVierteljaehrlichSchema = z.literal('Vierteljährliche Renten'); -export const RentenzahlungsweiseHalbjaehrlichSchema = z.literal('Halbjährliche Renten'); +export const RentenzahlungsweiseMonatlichSchema = + z.literal('Monatliche Renten'); +export const RentenzahlungsweiseVierteljaehrlichSchema = z.literal( + 'Vierteljährliche Renten' +); +export const RentenzahlungsweiseHalbjaehrlichSchema = z.literal( + 'Halbjährliche Renten' +); export const RentenzahlungsweiseJaehrlichSchema = z.literal('Jährliche Renten'); -export const RentenzahlungsweiseSchema = z.union([RentenzahlungsweiseMonatlichSchema, RentenzahlungsweiseVierteljaehrlichSchema, RentenzahlungsweiseHalbjaehrlichSchema, RentenzahlungsweiseJaehrlichSchema]); +export const RentenzahlungsweiseSchema = z.enum([ + RentenzahlungsweiseMonatlichSchema.value, + RentenzahlungsweiseVierteljaehrlichSchema.value, + RentenzahlungsweiseHalbjaehrlichSchema.value, + RentenzahlungsweiseJaehrlichSchema.value, +]); export type Rentenzahlungsweise = z.infer; + +export const rentenzahlungsweiseOpts: NxDropdownOption[] = Object.entries( + RentenzahlungsweiseSchema.enum +).map(([label, value]) => ({ + label, + value, +})); From 9d7013c5d6de8a4a0ddc5fbc18ad5a2e974515f0 Mon Sep 17 00:00:00 2001 From: "julio.fernandez" Date: Thu, 10 Apr 2025 08:37:15 +0200 Subject: [PATCH 22/26] Test Refactoring: Enhance tests and remove unused code --- .../quote-view-resolver.resolver.spec.ts | 2 +- .../__tests__/datepicker.component.spec.ts | 122 +++++++------ .../lib/date-picker/datepicker.component.html | 1 - .../lib/date-picker/datepicker.component.ts | 21 +-- .../__tests__/input-lib.component.spec.ts | 171 +++++------------- .../src/lib/input-lib/input-lib.component.ts | 2 +- .../store/__tests__/input.store.spec.ts | 43 +---- .../ui-lib/error-box/error-box.component.ts | 4 +- frontend/libs/view-lib/jest.config.ts | 4 +- frontend/libs/view-lib/project.json | 12 +- .../view-lib/__tests__/quote.store.spec.ts | 58 ++++++ .../__tests__/view-lib.component.spec.ts | 48 +++++ package-lock.json | 6 + package.json | 1 + shared/interfaces/src/lib/quote.interfaces.ts | 1 + 15 files changed, 267 insertions(+), 229 deletions(-) create mode 100644 frontend/libs/view-lib/src/lib/view-lib/__tests__/quote.store.spec.ts diff --git a/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts b/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts index 8cbe0df..52f6686 100644 --- a/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts +++ b/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts @@ -4,7 +4,7 @@ import { ResolveFn } from '@angular/router'; import { quoteViewResolver } from '../quote-view.resolver'; describe('quoteViewResolverResolver', () => { - const executeResolver: ResolveFn = (...resolverParameters) => + const executeResolver: ResolveFn = (...resolverParameters) => TestBed.runInInjectionContext(() => quoteViewResolver(...resolverParameters) ); diff --git a/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts b/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts index 522f116..f659110 100644 --- a/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts +++ b/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts @@ -1,52 +1,70 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NxDateAdapter } from '@aposin/ng-aquila/datefield'; -import moment from 'moment/moment'; - -import { DatepickerComponent } from '../datepicker.component'; - -describe('DatepickerComponent', () => { - let component: DatepickerComponent; - let fixture: ComponentFixture; - let dateAdapter: NxDateAdapter; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DatepickerComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(DatepickerComponent); - component = fixture.componentInstance; - - dateAdapter = TestBed.inject(NxDateAdapter); - - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set the input value correctly', () => { - const testDate = '2000-01-01'; - component.value = testDate; - fixture.detectChanges(); - - const parsedDate = dateAdapter.parse(testDate, 'YYYY-MM-DD', true); - expect(component['geburtsdatum']()).toEqual(parsedDate); - }); - - it('should emit dateChange event when date input changes', () => { - const dateChangeSpy = jest.spyOn(component.dateChange, 'emit'); - - const mockDate = moment('2000-01-01'); - const mockEvent = { - target: { - value: mockDate, - }, - }; - - component.updateDate(mockEvent as any); - - expect(dateChangeSpy).toHaveBeenCalledWith('2000-01-01'); - }); -}); +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import moment from 'moment/moment'; + +import { DatepickerComponent } from '../datepicker.component'; + +describe('DatepickerComponent', () => { + let component: DatepickerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DatepickerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DatepickerComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default empty value', () => { + expect(component.value).toBe(''); + }); + + it('should set min date to 100 years ago', () => { + const expectedMinDate = moment().subtract(100, 'years').startOf('day'); + const componentMinDate = component['minDate'].startOf('day'); + + expect(componentMinDate.isSame(expectedMinDate, 'day')).toBeTruthy(); + }); + + it('should set max date to 18 years ago', () => { + const expectedMaxDate = moment().subtract(18, 'years').startOf('day'); + const componentMaxDate = component['maxDate'].startOf('day'); + + expect(componentMaxDate.isSame(expectedMaxDate, 'day')).toBeTruthy(); + }); + + it('should call onChange when value changes', () => { + const date = '2000-01-01'; + + const onChangeMock = jest.fn(); + component.registerOnChange(onChangeMock); + + component.value = moment(date); + + expect(onChangeMock).toHaveBeenCalledWith(date); + expect(component.value).toBe(date); + }); + + it('should handle writeValue correctly', () => { + const date = '2001-02-03'; + component.writeValue(moment(date)); + + expect(component.value).toBe(date); + }); + + it('should call onTouched when value changes', () => { + const onTouchedMock = jest.fn(); + component.registerOnTouched(onTouchedMock); + + component.value = moment('2000-01-01'); + + expect(onTouchedMock).toHaveBeenCalled(); + }); +}); diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html index ea5e024..794cfb6 100644 --- a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html +++ b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html @@ -6,7 +6,6 @@ nxInput [datepicker]="datePicker" [(ngModel)]="value" - [disabled]="isDisabled" /> MM/DD/YYYY Please enter a valid date. diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts index 01d9500..28b4794 100644 --- a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts +++ b/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts @@ -47,29 +47,30 @@ import moment, { Moment } from 'moment/moment'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatepickerComponent implements ControlValueAccessor { - protected isDisabled: boolean = false; - private _value: string = ''; - private onChange: any = () => {}; - private onTouched: any = () => {}; - protected readonly adapter = inject(NxDateAdapter); protected minDate = moment().subtract(100, 'years'); protected maxDate = moment().subtract(18, 'years'); + private _value: string = ''; + + private onChange: any = () => {}; + private onTouched: any = () => {}; + get value(): string { return this._value; } set value(val: Moment) { this._value = val?.format('YYYY-MM-DD'); + this.onChange(val?.format('YYYY-MM-DD')); this.onTouched(); } - writeValue(value: string): void { - if (value !== undefined) { - this._value = ''; + writeValue(value: Moment): void { + if (value) { + this._value = value.format('YYYY-MM-DD'); } } @@ -81,7 +82,5 @@ export class DatepickerComponent implements ControlValueAccessor { this.onTouched = fn; } - setDisabledState?(isDisabled: boolean): void { - this.isDisabled = isDisabled; - } + setDisabledState?(_: boolean): void {} } diff --git a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts index 8822609..a1e81e4 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts @@ -1,154 +1,81 @@ -import { HttpClient, provideHttpClient } from '@angular/common/http'; +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; -import { of } from 'rxjs'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; import { InputLibComponent } from '../../../index'; import { InputStore } from '../store/input.store'; -import { Input, InputState } from '../store/input.store.interfaces'; import { QuoteService } from '../store/services/quote.service'; -jest.mock('../store/input.store'); -jest.mock('../store/services/quote.service'); - describe('InputLibComponent', () => { let component: InputLibComponent; let fixture: ComponentFixture; - let inputStore: any; - let _quoteService: QuoteService; + + // @ts-ignore + let consoleSpy: any; + + const mockInputStore = { + uiState: jest.fn().mockReturnValue({ + geburtsdatum: { value: '' }, + leistungsVorgabe: { value: '' }, + beitrag: { value: '' }, + berechnungDerLaufzeit: { value: '' }, + laufzeit: { value: '' }, + beitragszahlungsweise: { value: '' }, + rentenzahlungsweise: { value: '' }, + }), + updateInputs: jest.fn().mockResolvedValue(undefined), + calculate: jest.fn().mockResolvedValue('1234'), + processErrors: jest.fn(), + }; + + const mockRouter = { + navigate: jest.fn().mockResolvedValue(true), + }; beforeEach(async () => { - const mockHttp = { - post: jest.fn(), - } as Partial; - const mockQuoteService = { - calculateQuote: jest.fn().mockReturnValue( - of({ - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2024-01-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 30, - }, - leistungsmerkmale: { - garantierteMindestrente: 1000, - einmaligesGarantiekapital: 100000, - todesfallleistungAbAltersrentenbezug: 50000, - }, - beitrag: { - einmalbeitrag: 50000, - beitragsdynamik: '3%', - }, - }) - ), - http: mockHttp as HttpClient, - }; - const mockState: InputState = { - geburtsdatum: { value: '2002-02-04', valid: true, error: null }, - leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, - beitrag: { value: 1000, valid: true, error: null }, - berechnungDerLaufzeit: { - value: 'Alter bei Rentenbeginn', - valid: true, - error: null, - }, - laufzeit: { value: 10, valid: true, error: null }, - beitragszahlungsweise: { - value: 'Einmalbeitrag', - valid: true, - error: null, - }, - rentenzahlungsweise: { - value: 'Monatliche Renten', - valid: true, - error: null, - }, - // quote: { - // basisdaten: { - // geburtsdatum: '', - // versicherungsbeginn: '', - // garantieniveau: '', - // alterBeiRentenbeginn: 0, - // aufschubdauer: 0, - // beitragszahlungsdauer: 0 - // }, - // leistungsmerkmale: { - // garantierteMindestrente: 0, - // einmaligesGarantiekapital: 0, - // todesfallleistungAbAltersrentenbezug: 0 - // }, - // beitrag: { - // einmalbeitrag: 0, - // beitragsdynamik: '' - // }, - // }, - }; + // Prevent 'Could not parse CSS stylesheet' exception from running the test. Apparently a bug with nx-spinner css library + consoleSpy = jest + .spyOn(global.console, 'error') + .mockImplementation((message) => { + if (!message.toString().includes('Could not parse CSS stylesheet')) { + global.console.warn(message); + } + }); await TestBed.configureTestingModule({ - imports: [InputLibComponent, NxSpinnerComponent], + imports: [ReactiveFormsModule, NoopAnimationsModule, InputLibComponent], providers: [ provideHttpClient(), - { provide: QuoteService, useValue: mockQuoteService }, + { + provide: QuoteService, + useValue: { calculateQuote: jest.fn(), fetchQuote: jest.fn() }, + }, { provide: InputStore, - useValue: { - updateInputs: jest.fn(), - calculate: jest.fn(), - uiState: jest.fn(() => mockState), - }, + useValue: mockInputStore, + }, + { + provide: Router, + useValue: mockRouter, }, ], }).compileComponents(); fixture = TestBed.createComponent(InputLibComponent); component = fixture.componentInstance; - inputStore = TestBed.inject(InputStore); - _quoteService = TestBed.inject(QuoteService); - fixture.detectChanges(); }); + afterAll(() => consoleSpy.mockRestore()); + it('should create', () => { expect(component).toBeTruthy(); }); - it('should update inputs through the store', async () => { - const input: Input = { - key: 'beitrag', - value: 2000, - }; - - await component.updateInputs(input); - - expect(inputStore.updateInputs).toHaveBeenCalledWith(input); - }); - - it('should calculate through the store', async () => { - await component.calculate(); - // TOOO: Fix text with "Could not parse CSS stylesheet" error - expect(inputStore.calculate).toHaveBeenCalled(); - }); - - it('should handle string inputs', async () => { - const input: Input = { - key: 'leistungsVorgabe', - value: 'Rente', - }; - - await component.updateInputs(input); - - expect(inputStore.updateInputs).toHaveBeenCalledWith(input); - }); - - it('should handle numeric inputs', async () => { - const input: Input = { - key: 'laufzeit', - value: 15, - }; - - await component.updateInputs(input); + it('should calculate through the store', () => { + component.calculate(); - expect(inputStore.updateInputs).toHaveBeenCalledWith(input); + expect(mockInputStore.calculate).toHaveBeenCalled(); }); }); diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index cea8e45..769c889 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -66,7 +66,7 @@ export class InputLibComponent implements OnInit { protected readonly beitragszahlungsweiseOpts = beitragszahlungsweiseOpts; protected readonly rentenzahlungsweiseOpts = rentenzahlungsweiseOpts; - profile = this.formBuilder.group({ + protected profile = this.formBuilder.group({ geburtsdatum: [this.inputStore.uiState()['geburtsdatum'].value], leistungsVorgabe: [this.inputStore.uiState()['leistungsVorgabe'].value], beitrag: [this.inputStore.uiState()['beitrag'].value], diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts index b425fd6..7c15d5a 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { fakerEN } from '@faker-js/faker'; -import { QuoteResponseDto } from '@target/interfaces'; +import { QuoteCreateResponseDto } from '@target/interfaces'; import { InputDtoSchema } from '@target/validations'; import { of, throwError } from 'rxjs'; @@ -17,27 +17,8 @@ describe('InputStore', () => { let store: any; let quoteService: jest.Mocked; - const date = fakerEN.date.past({ years: 20 }); - const geburtsdatum = `${date.getUTCFullYear()}-${date.getMonth()}-${date.getDate()}`; - - const mockQuoteResponse: QuoteResponseDto = { - basisdaten: { - geburtsdatum, - versicherungsbeginn: '2024-01-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 10, - }, - leistungsmerkmale: { - garantierteMindestrente: 50000, - einmaligesGarantiekapital: 25000, - todesfallleistungAbAltersrentenbezug: 40000, - }, - beitrag: { - einmalbeitrag: 0, - beitragsdynamik: '1,5%', - }, + const quoteCreateResponseDtoMock: QuoteCreateResponseDto = { + id: fakerEN.string.alpha({ length: 15 }), }; beforeEach(() => { @@ -54,7 +35,7 @@ describe('InputStore', () => { describe('updateInputs', () => { it('should update state when validation succeeds', async () => { - const input = { key: 'beitrag', value: 2000 }; + const input = [{ key: 'beitrag', value: 2000 }]; (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true, @@ -68,7 +49,7 @@ describe('InputStore', () => { }); it('should update state with validation errors when validation fails', async () => { - const input = { key: 'beitrag', value: -1 }; + const input = [{ key: 'beitrag', value: -1 }]; (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: false, @@ -90,11 +71,12 @@ describe('InputStore', () => { (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true, }); - quoteService.calculateQuote.mockReturnValue(of(mockQuoteResponse)); + quoteService.calculateQuote.mockReturnValue( + of(quoteCreateResponseDtoMock) + ); await (store as any).calculate(); - expect(store.uiState().quote).toEqual(mockQuoteResponse); expect(quoteService.calculateQuote).toHaveBeenCalled(); }); @@ -109,21 +91,16 @@ describe('InputStore', () => { }); it('should handle API errors', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true, }); - const error = new Error('API Error'); + const error = new Error('Invalid input'); quoteService.calculateQuote.mockReturnValue(throwError(() => error)); - await (store as any).calculate(); + await expect((store as any).calculate()).rejects.toThrow(error); - expect(consoleSpy).toHaveBeenCalledWith(error); expect(quoteService.calculateQuote).toHaveBeenCalled(); - - consoleSpy.mockRestore(); }); }); }); diff --git a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts index acfbc7f..eba9385 100644 --- a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts +++ b/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts @@ -23,11 +23,11 @@ export class ErrorBoxComponent { this.apiErrorMessage.set({ message: [] } as ApiError); } - const error: ApiError = Array.isArray(value.message) + const error: ApiError = Array.isArray(value?.message) ? (value as ApiError) : { ...value, - message: value.message ? [value.message] : [], + message: value?.message ? [value.message] : [], }; this.apiErrorMessage.set(error); diff --git a/frontend/libs/view-lib/jest.config.ts b/frontend/libs/view-lib/jest.config.ts index 96f4402..e185fe7 100644 --- a/frontend/libs/view-lib/jest.config.ts +++ b/frontend/libs/view-lib/jest.config.ts @@ -1,8 +1,8 @@ export default { displayName: 'view-lib', - preset: '../jest.preset.js', + preset: '../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../coverage/view-lib', + coverageDirectory: '../../../coverage/frontend/libs/view-lib', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', diff --git a/frontend/libs/view-lib/project.json b/frontend/libs/view-lib/project.json index 195cbd8..2405cec 100644 --- a/frontend/libs/view-lib/project.json +++ b/frontend/libs/view-lib/project.json @@ -1,16 +1,20 @@ { "name": "view-lib", "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "view-lib/src", + "sourceRoot": "frontend/libs/view-lib/src", "prefix": "lib", "projectType": "library", - "tags": [], + "tags": [ + "ui" + ], "targets": { "test": { "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], "options": { - "jestConfig": "view-lib/jest.config.ts" + "jestConfig": "frontend/libs/view-lib/jest.config.ts" } }, "lint": { diff --git a/frontend/libs/view-lib/src/lib/view-lib/__tests__/quote.store.spec.ts b/frontend/libs/view-lib/src/lib/view-lib/__tests__/quote.store.spec.ts new file mode 100644 index 0000000..60213af --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/__tests__/quote.store.spec.ts @@ -0,0 +1,58 @@ +import { TestBed } from '@angular/core/testing'; +import { fakerEN } from '@faker-js/faker'; +import { QuoteResponseDto } from '@target/interfaces'; +import { of } from 'rxjs'; + +import { QuoteService } from '../../../../../input-lib/src/lib/input-lib/store/services/quote.service'; +import { QuoteStore } from '../store/quote.store'; + +describe('QuoteStore', () => { + let store: any; + let quoteService: QuoteService; + + const mockQuoteResponse: QuoteResponseDto = { + id: fakerEN.string.alpha({ length: 15 }), + basisdaten: { + geburtsdatum: '2000-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 50000, + einmaligesGarantiekapital: 25000, + todesfallleistungAbAltersrentenbezug: 40000, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(() => { + quoteService = { + fetchQuote: jest.fn(), + } as unknown as jest.Mocked; + + TestBed.configureTestingModule({ + providers: [{ provide: QuoteService, useValue: quoteService }], + }); + + store = TestBed.inject(QuoteStore); + }); + + it('should fetch quote and update state', async () => { + jest + .spyOn(quoteService, 'fetchQuote') + .mockReturnValue(of(mockQuoteResponse)); + + await store.fetchQuote(mockQuoteResponse.id); + + expect(quoteService.fetchQuote).toHaveBeenCalledWith(mockQuoteResponse.id); + expect(quoteService.fetchQuote).toHaveBeenCalledTimes(1); + + expect(store.quoteState()).toEqual(mockQuoteResponse); + }); +}); diff --git a/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts index 105b29b..0fc7ea7 100644 --- a/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts +++ b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts @@ -1,14 +1,56 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { QuoteStore } from '../store/quote.store'; import { ViewLibComponent } from '../view-lib.component'; describe('ViewLibComponent', () => { let component: ViewLibComponent; let fixture: ComponentFixture; + // @ts-ignore + let consoleSpy: any; + + const mockQuoteState = { + basisdaten: { + geburtsdatum: '2020-01-01', + versicherungsbeginn: '2025-01-01', + garantieniveau: '', + alterBeiRentenbeginn: 0, + aufschubdauer: 0, + beitragszahlungsdauer: 0, + }, + leistungsmerkmale: { + garantierteMindestrente: 0, + einmaligesGarantiekapital: 0, + todesfallleistungAbAltersrentenbezug: 0, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '', + }, + }; + beforeEach(async () => { + // Prevent 'Could not parse CSS stylesheet' exception from running the test. Apparently a bug with nx-spinner css library + consoleSpy = jest + .spyOn(global.console, 'error') + .mockImplementation((message) => { + if (!message.toString().includes('Could not parse CSS stylesheet')) { + global.console.warn(message); + } + }); + await TestBed.configureTestingModule({ imports: [ViewLibComponent], + providers: [ + { + provide: QuoteStore, + useValue: { + quoteState: jest.fn().mockReturnValue(mockQuoteState), + fetchQuote: jest.fn().mockResolvedValue(undefined), + }, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(ViewLibComponent); @@ -16,7 +58,13 @@ describe('ViewLibComponent', () => { fixture.detectChanges(); }); + afterAll(() => consoleSpy.mockRestore()); + it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have state property with QuoteStore data', () => { + expect(component['state']).toEqual(mockQuoteState); + }); }); diff --git a/package-lock.json b/package-lock.json index 0981d7a..b5c7cd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", "axios": "^1.6.0", + "dayjs": "^1.11.13", "es-module-shims": "^1.5.12", "moment": "^2.30.1", "reflect-metadata": "^0.1.13", @@ -12037,6 +12038,11 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index 2e43dac..330a707 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", "axios": "^1.6.0", + "dayjs": "^1.11.13", "es-module-shims": "^1.5.12", "moment": "^2.30.1", "reflect-metadata": "^0.1.13", diff --git a/shared/interfaces/src/lib/quote.interfaces.ts b/shared/interfaces/src/lib/quote.interfaces.ts index a8415a8..90e231f 100644 --- a/shared/interfaces/src/lib/quote.interfaces.ts +++ b/shared/interfaces/src/lib/quote.interfaces.ts @@ -3,6 +3,7 @@ import { InputDto } from '@target/validations'; export interface QuoteRequestDto extends InputDto {} export interface QuoteResponseDto { + id?: string; basisdaten: QuoteBasisdatenDto; leistungsmerkmale: QuoteLeistungsmerkmaleDto; beitrag: QuoteBeitragDto; From 9f7febc90480a4aac9ec1347071eee26e463fcdb Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Thu, 10 Apr 2025 11:54:50 +0200 Subject: [PATCH 23/26] Code Cleanup: clean up interfaces and arrange FE libs --- frontend/apps/shell/src/app/app.routes.ts | 3 +- frontend/libs/date-picker-lib/README.md | 7 ++++ .../eslint.config.cjs | 0 frontend/libs/date-picker-lib/jest.config.ts | 21 ++++++++++++ frontend/libs/date-picker-lib/project.json | 20 +++++++++++ frontend/libs/date-picker-lib/src/index.ts | 1 + .../__tests__/datepicker.component.spec.ts | 0 .../datepicker.component.html | 0 .../date-picker-lib}/datepicker.component.ts | 0 .../src/test-setup.ts | 0 frontend/libs/date-picker-lib/tsconfig.json | 29 ++++++++++++++++ .../tsconfig.lib.json | 2 +- .../tsconfig.spec.json | 6 ++-- .../libs/{ui-lib => error-box-lib}/README.md | 4 +-- frontend/libs/error-box-lib/eslint.config.cjs | 34 +++++++++++++++++++ .../{ui-lib => error-box-lib}/jest.config.ts | 4 +-- .../{ui-lib => error-box-lib}/project.json | 8 ++--- frontend/libs/error-box-lib/src/index.ts | 1 + .../__tests__/error-box.component.spec.ts | 2 +- .../error-box-lib}/error-box.component.html | 0 .../lib/error-box-lib}/error-box.component.ts | 0 frontend/libs/error-box-lib/src/test-setup.ts | 6 ++++ .../{ui-lib => error-box-lib}/tsconfig.json | 0 frontend/libs/error-box-lib/tsconfig.lib.json | 17 ++++++++++ .../libs/error-box-lib/tsconfig.spec.json | 16 +++++++++ frontend/libs/input-lib/src/index.ts | 4 +-- .../src/lib/input-lib/input-lib.component.ts | 6 ++-- frontend/libs/quote-resolver-lib/README.md | 7 ++++ .../libs/quote-resolver-lib/eslint.config.cjs | 34 +++++++++++++++++++ .../libs/quote-resolver-lib/jest.config.ts | 21 ++++++++++++ frontend/libs/quote-resolver-lib/project.json | 20 +++++++++++ frontend/libs/quote-resolver-lib/src/index.ts | 1 + .../quote-view-resolver.resolver.spec.ts | 0 .../quote-view.resolver.ts | 3 +- .../libs/quote-resolver-lib/src/test-setup.ts | 6 ++++ .../libs/quote-resolver-lib/tsconfig.json | 28 +++++++++++++++ .../libs/quote-resolver-lib/tsconfig.lib.json | 17 ++++++++++ .../quote-resolver-lib/tsconfig.spec.json | 16 +++++++++ frontend/libs/ui-lib/src/index.ts | 1 - frontend/libs/view-lib/src/index.ts | 1 + shared/interfaces/src/lib/quote.interfaces.ts | 12 +++---- .../src/lib/input/beitragszahlungsweise.ts | 1 - .../src/lib/input/berechnung-der-laufzeit.ts | 1 - .../src/lib/input/leistungsvorgabe.ts | 1 - .../src/lib/input/rentenzahlungsweise.ts | 1 - tsconfig.base.json | 5 +++ 46 files changed, 332 insertions(+), 35 deletions(-) create mode 100644 frontend/libs/date-picker-lib/README.md rename frontend/libs/{ui-lib => date-picker-lib}/eslint.config.cjs (100%) create mode 100644 frontend/libs/date-picker-lib/jest.config.ts create mode 100644 frontend/libs/date-picker-lib/project.json create mode 100644 frontend/libs/date-picker-lib/src/index.ts rename frontend/libs/{input-lib/src/lib/date-picker => date-picker-lib/src/lib/date-picker-lib}/__tests__/datepicker.component.spec.ts (100%) rename frontend/libs/{input-lib/src/lib/date-picker => date-picker-lib/src/lib/date-picker-lib}/datepicker.component.html (100%) rename frontend/libs/{input-lib/src/lib/date-picker => date-picker-lib/src/lib/date-picker-lib}/datepicker.component.ts (100%) rename frontend/libs/{ui-lib => date-picker-lib}/src/test-setup.ts (100%) create mode 100644 frontend/libs/date-picker-lib/tsconfig.json rename frontend/libs/{ui-lib => date-picker-lib}/tsconfig.lib.json (88%) rename frontend/libs/{ui-lib => date-picker-lib}/tsconfig.spec.json (77%) rename frontend/libs/{ui-lib => error-box-lib}/README.md (52%) create mode 100644 frontend/libs/error-box-lib/eslint.config.cjs rename frontend/libs/{ui-lib => error-box-lib}/jest.config.ts (85%) rename frontend/libs/{ui-lib => error-box-lib}/project.json (67%) create mode 100644 frontend/libs/error-box-lib/src/index.ts rename frontend/libs/{ui-lib/src/lib/ui-lib/error-box => error-box-lib/src/lib/error-box-lib}/__tests__/error-box.component.spec.ts (95%) rename frontend/libs/{ui-lib/src/lib/ui-lib/error-box => error-box-lib/src/lib/error-box-lib}/error-box.component.html (100%) rename frontend/libs/{ui-lib/src/lib/ui-lib/error-box => error-box-lib/src/lib/error-box-lib}/error-box.component.ts (100%) create mode 100644 frontend/libs/error-box-lib/src/test-setup.ts rename frontend/libs/{ui-lib => error-box-lib}/tsconfig.json (100%) create mode 100644 frontend/libs/error-box-lib/tsconfig.lib.json create mode 100644 frontend/libs/error-box-lib/tsconfig.spec.json create mode 100644 frontend/libs/quote-resolver-lib/README.md create mode 100644 frontend/libs/quote-resolver-lib/eslint.config.cjs create mode 100644 frontend/libs/quote-resolver-lib/jest.config.ts create mode 100644 frontend/libs/quote-resolver-lib/project.json create mode 100644 frontend/libs/quote-resolver-lib/src/index.ts rename frontend/{apps/shell/src/app/resolvers => libs/quote-resolver-lib/src/lib/quote-resolver-lib}/__tests__/quote-view-resolver.resolver.spec.ts (100%) rename frontend/{apps/shell/src/app/resolvers => libs/quote-resolver-lib/src/lib/quote-resolver-lib}/quote-view.resolver.ts (72%) create mode 100644 frontend/libs/quote-resolver-lib/src/test-setup.ts create mode 100644 frontend/libs/quote-resolver-lib/tsconfig.json create mode 100644 frontend/libs/quote-resolver-lib/tsconfig.lib.json create mode 100644 frontend/libs/quote-resolver-lib/tsconfig.spec.json delete mode 100644 frontend/libs/ui-lib/src/index.ts diff --git a/frontend/apps/shell/src/app/app.routes.ts b/frontend/apps/shell/src/app/app.routes.ts index a200b81..7b3bd0d 100644 --- a/frontend/apps/shell/src/app/app.routes.ts +++ b/frontend/apps/shell/src/app/app.routes.ts @@ -1,9 +1,8 @@ import { Route } from '@angular/router'; import { InputLibComponent } from '@target/input-lib'; +import { quoteViewResolver } from '@target/quote-resolver-lib'; import { ViewLibComponent } from '@target/view-lib'; -import { quoteViewResolver } from './resolvers/quote-view.resolver'; - const ROUTES = { INPUTS: 'inputs', VIEW: 'view/:id', diff --git a/frontend/libs/date-picker-lib/README.md b/frontend/libs/date-picker-lib/README.md new file mode 100644 index 0000000..c4a547e --- /dev/null +++ b/frontend/libs/date-picker-lib/README.md @@ -0,0 +1,7 @@ +# date-picker-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test date-picker-lib` to execute the unit tests. diff --git a/frontend/libs/ui-lib/eslint.config.cjs b/frontend/libs/date-picker-lib/eslint.config.cjs similarity index 100% rename from frontend/libs/ui-lib/eslint.config.cjs rename to frontend/libs/date-picker-lib/eslint.config.cjs diff --git a/frontend/libs/date-picker-lib/jest.config.ts b/frontend/libs/date-picker-lib/jest.config.ts new file mode 100644 index 0000000..cff5969 --- /dev/null +++ b/frontend/libs/date-picker-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'date-picker-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/date-picker-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/date-picker-lib/project.json b/frontend/libs/date-picker-lib/project.json new file mode 100644 index 0000000..acdd697 --- /dev/null +++ b/frontend/libs/date-picker-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "date-picker-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/date-picker-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["ui"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/date-picker-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/date-picker-lib/src/index.ts b/frontend/libs/date-picker-lib/src/index.ts new file mode 100644 index 0000000..2baf6e9 --- /dev/null +++ b/frontend/libs/date-picker-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/date-picker-lib/datepicker.component' diff --git a/frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/__tests__/datepicker.component.spec.ts similarity index 100% rename from frontend/libs/input-lib/src/lib/date-picker/__tests__/datepicker.component.spec.ts rename to frontend/libs/date-picker-lib/src/lib/date-picker-lib/__tests__/datepicker.component.spec.ts diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.html similarity index 100% rename from frontend/libs/input-lib/src/lib/date-picker/datepicker.component.html rename to frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.html diff --git a/frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.ts similarity index 100% rename from frontend/libs/input-lib/src/lib/date-picker/datepicker.component.ts rename to frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.ts diff --git a/frontend/libs/ui-lib/src/test-setup.ts b/frontend/libs/date-picker-lib/src/test-setup.ts similarity index 100% rename from frontend/libs/ui-lib/src/test-setup.ts rename to frontend/libs/date-picker-lib/src/test-setup.ts diff --git a/frontend/libs/date-picker-lib/tsconfig.json b/frontend/libs/date-picker-lib/tsconfig.json new file mode 100644 index 0000000..866de3d --- /dev/null +++ b/frontend/libs/date-picker-lib/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/ui-lib/tsconfig.lib.json b/frontend/libs/date-picker-lib/tsconfig.lib.json similarity index 88% rename from frontend/libs/ui-lib/tsconfig.lib.json rename to frontend/libs/date-picker-lib/tsconfig.lib.json index 64216c9..9b49be7 100644 --- a/frontend/libs/ui-lib/tsconfig.lib.json +++ b/frontend/libs/date-picker-lib/tsconfig.lib.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../dist/out-tsc", + "outDir": "../../../dist/out-tsc", "declaration": true, "declarationMap": true, "inlineSources": true, diff --git a/frontend/libs/ui-lib/tsconfig.spec.json b/frontend/libs/date-picker-lib/tsconfig.spec.json similarity index 77% rename from frontend/libs/ui-lib/tsconfig.spec.json rename to frontend/libs/date-picker-lib/tsconfig.spec.json index aad9b06..f858ef7 100644 --- a/frontend/libs/ui-lib/tsconfig.spec.json +++ b/frontend/libs/date-picker-lib/tsconfig.spec.json @@ -1,14 +1,12 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../dist/out-tsc", + "outDir": "../../../dist/out-tsc", "module": "commonjs", "target": "es2016", "types": ["jest", "node"] }, - "files": [ - "src/test-setup.ts" - ], + "files": ["src/test-setup.ts"], "include": [ "jest.config.ts", "src/**/*.test.ts", diff --git a/frontend/libs/ui-lib/README.md b/frontend/libs/error-box-lib/README.md similarity index 52% rename from frontend/libs/ui-lib/README.md rename to frontend/libs/error-box-lib/README.md index ea5b39f..5cfb36d 100644 --- a/frontend/libs/ui-lib/README.md +++ b/frontend/libs/error-box-lib/README.md @@ -1,7 +1,7 @@ -# ui-lib +# error-box-lib This library was generated with [Nx](https://nx.dev). ## Running unit tests -Run `nx test ui-lib` to execute the unit tests. +Run `nx test error-box-lib` to execute the unit tests. diff --git a/frontend/libs/error-box-lib/eslint.config.cjs b/frontend/libs/error-box-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/error-box-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/ui-lib/jest.config.ts b/frontend/libs/error-box-lib/jest.config.ts similarity index 85% rename from frontend/libs/ui-lib/jest.config.ts rename to frontend/libs/error-box-lib/jest.config.ts index 35dac7d..6425c1a 100644 --- a/frontend/libs/ui-lib/jest.config.ts +++ b/frontend/libs/error-box-lib/jest.config.ts @@ -1,8 +1,8 @@ export default { - displayName: 'ui-lib', + displayName: 'error-box-lib', preset: '../../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../coverage/ui-lib', + coverageDirectory: '../../../coverage/frontend/libs/error-box-lib', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', diff --git a/frontend/libs/ui-lib/project.json b/frontend/libs/error-box-lib/project.json similarity index 67% rename from frontend/libs/ui-lib/project.json rename to frontend/libs/error-box-lib/project.json index 247b8fe..92767a5 100644 --- a/frontend/libs/ui-lib/project.json +++ b/frontend/libs/error-box-lib/project.json @@ -1,16 +1,16 @@ { - "name": "ui-lib", + "name": "error-box-lib", "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "ui-lib/src", + "sourceRoot": "frontend/libs/error-box-lib/src", "prefix": "lib", "projectType": "library", - "tags": [], + "tags": ["ui"], "targets": { "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { - "jestConfig": "ui-lib/jest.config.ts" + "jestConfig": "frontend/libs/error-box-lib/jest.config.ts" } }, "lint": { diff --git a/frontend/libs/error-box-lib/src/index.ts b/frontend/libs/error-box-lib/src/index.ts new file mode 100644 index 0000000..9fa9e98 --- /dev/null +++ b/frontend/libs/error-box-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/error-box-lib/error-box.component'; diff --git a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/__tests__/error-box.component.spec.ts b/frontend/libs/error-box-lib/src/lib/error-box-lib/__tests__/error-box.component.spec.ts similarity index 95% rename from frontend/libs/ui-lib/src/lib/ui-lib/error-box/__tests__/error-box.component.spec.ts rename to frontend/libs/error-box-lib/src/lib/error-box-lib/__tests__/error-box.component.spec.ts index 4f5629c..bc20021 100644 --- a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/__tests__/error-box.component.spec.ts +++ b/frontend/libs/error-box-lib/src/lib/error-box-lib/__tests__/error-box.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ErrorBoxComponent } from '../../../../index'; +import { ErrorBoxComponent } from '../error-box.component'; describe('ErrorBoxComponent', () => { let component: ErrorBoxComponent; diff --git a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.html b/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.html similarity index 100% rename from frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.html rename to frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.html diff --git a/frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts b/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.ts similarity index 100% rename from frontend/libs/ui-lib/src/lib/ui-lib/error-box/error-box.component.ts rename to frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.ts diff --git a/frontend/libs/error-box-lib/src/test-setup.ts b/frontend/libs/error-box-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/error-box-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/ui-lib/tsconfig.json b/frontend/libs/error-box-lib/tsconfig.json similarity index 100% rename from frontend/libs/ui-lib/tsconfig.json rename to frontend/libs/error-box-lib/tsconfig.json diff --git a/frontend/libs/error-box-lib/tsconfig.lib.json b/frontend/libs/error-box-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/error-box-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/error-box-lib/tsconfig.spec.json b/frontend/libs/error-box-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/error-box-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/input-lib/src/index.ts b/frontend/libs/input-lib/src/index.ts index 836bf52..e3d8778 100644 --- a/frontend/libs/input-lib/src/index.ts +++ b/frontend/libs/input-lib/src/index.ts @@ -1,3 +1 @@ -import { InputLibComponent } from "./lib/input-lib/input-lib.component"; - -export { InputLibComponent }; +export * from './lib/input-lib/input-lib.component'; diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index 769c889..c7c6f75 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -16,7 +16,6 @@ import { NxInputModule } from '@aposin/ng-aquila/input'; import { NxIsoDateModule } from '@aposin/ng-aquila/iso-date-adapter'; import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; import { ApiErrorResponse } from '@target/interfaces'; -import { ErrorBoxComponent } from '@target/ui-lib'; import { beitragszahlungsweiseOpts, berechnungDerLaufzeitOpts, @@ -25,9 +24,10 @@ import { } from '@target/validations'; import { debounceTime } from 'rxjs'; -import { DatepickerComponent } from '../date-picker/datepicker.component'; +import { DatepickerComponent } from '../../../../date-picker-lib/src/lib/date-picker-lib/datepicker.component'; import { InputStore } from './store/input.store'; import { Input } from './store/input.store.interfaces'; +import { ErrorBoxComponent } from '../../../../error-box-lib/src/lib/error-box-lib/error-box.component'; @Component({ selector: 'lib-input-lib', @@ -44,10 +44,10 @@ import { Input } from './store/input.store.interfaces'; NxErrorModule, NxButtonModule, NxSpinnerComponent, - ErrorBoxComponent, FormsModule, ReactiveFormsModule, DatepickerComponent, + ErrorBoxComponent, ], templateUrl: './input-lib.component.html', }) diff --git a/frontend/libs/quote-resolver-lib/README.md b/frontend/libs/quote-resolver-lib/README.md new file mode 100644 index 0000000..2cdc009 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/README.md @@ -0,0 +1,7 @@ +# quote-resolver-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test quote-resolver-lib` to execute the unit tests. diff --git a/frontend/libs/quote-resolver-lib/eslint.config.cjs b/frontend/libs/quote-resolver-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/quote-resolver-lib/jest.config.ts b/frontend/libs/quote-resolver-lib/jest.config.ts new file mode 100644 index 0000000..5d54c69 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'quote-resolver-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/quote-resolver-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/quote-resolver-lib/project.json b/frontend/libs/quote-resolver-lib/project.json new file mode 100644 index 0000000..e494aac --- /dev/null +++ b/frontend/libs/quote-resolver-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "quote-resolver-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/quote-resolver-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["resolver"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/quote-resolver-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/quote-resolver-lib/src/index.ts b/frontend/libs/quote-resolver-lib/src/index.ts new file mode 100644 index 0000000..e6d5a81 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/quote-resolver-lib/quote-view.resolver' diff --git a/frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/__tests__/quote-view-resolver.resolver.spec.ts similarity index 100% rename from frontend/apps/shell/src/app/resolvers/__tests__/quote-view-resolver.resolver.spec.ts rename to frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/__tests__/quote-view-resolver.resolver.spec.ts diff --git a/frontend/apps/shell/src/app/resolvers/quote-view.resolver.ts b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts similarity index 72% rename from frontend/apps/shell/src/app/resolvers/quote-view.resolver.ts rename to frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts index f2bfb66..354f9ae 100644 --- a/frontend/apps/shell/src/app/resolvers/quote-view.resolver.ts +++ b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts @@ -1,7 +1,6 @@ import { inject } from '@angular/core'; import { ResolveFn } from '@angular/router'; - -import { QuoteStore } from '../../../../../libs/view-lib/src/lib/view-lib/store/quote.store'; +import { QuoteStore } from '@target/view-lib'; export const quoteViewResolver: ResolveFn = (route, state) => { const quoteStore = inject(QuoteStore); diff --git a/frontend/libs/quote-resolver-lib/src/test-setup.ts b/frontend/libs/quote-resolver-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/quote-resolver-lib/tsconfig.json b/frontend/libs/quote-resolver-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/quote-resolver-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/quote-resolver-lib/tsconfig.lib.json b/frontend/libs/quote-resolver-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/quote-resolver-lib/tsconfig.spec.json b/frontend/libs/quote-resolver-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/ui-lib/src/index.ts b/frontend/libs/ui-lib/src/index.ts deleted file mode 100644 index ef301c5..0000000 --- a/frontend/libs/ui-lib/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/ui-lib/error-box/error-box.component'; diff --git a/frontend/libs/view-lib/src/index.ts b/frontend/libs/view-lib/src/index.ts index 86fd00c..e2e3022 100644 --- a/frontend/libs/view-lib/src/index.ts +++ b/frontend/libs/view-lib/src/index.ts @@ -1 +1,2 @@ +export * from './lib/view-lib/store/quote.store'; export * from './lib/view-lib/view-lib.component'; diff --git a/shared/interfaces/src/lib/quote.interfaces.ts b/shared/interfaces/src/lib/quote.interfaces.ts index 90e231f..7349b0e 100644 --- a/shared/interfaces/src/lib/quote.interfaces.ts +++ b/shared/interfaces/src/lib/quote.interfaces.ts @@ -4,16 +4,16 @@ export interface QuoteRequestDto extends InputDto {} export interface QuoteResponseDto { id?: string; - basisdaten: QuoteBasisdatenDto; - leistungsmerkmale: QuoteLeistungsmerkmaleDto; - beitrag: QuoteBeitragDto; + basisdaten: IQuoteBasisdaten; + leistungsmerkmale: IQuoteLeistungsmerkmale; + beitrag: IQuoteBeitrag; } export interface QuoteCreateResponseDto { id: string; } -interface QuoteBasisdatenDto { +interface IQuoteBasisdaten { geburtsdatum: string; versicherungsbeginn: string; garantieniveau: string; @@ -22,13 +22,13 @@ interface QuoteBasisdatenDto { beitragszahlungsdauer: number; } -interface QuoteLeistungsmerkmaleDto { +interface IQuoteLeistungsmerkmale { garantierteMindestrente: number; einmaligesGarantiekapital: number; todesfallleistungAbAltersrentenbezug: number; } -interface QuoteBeitragDto { +interface IQuoteBeitrag { einmalbeitrag: number; beitragsdynamik: string; } diff --git a/shared/validations/src/lib/input/beitragszahlungsweise.ts b/shared/validations/src/lib/input/beitragszahlungsweise.ts index 1eacd80..16b9f3e 100644 --- a/shared/validations/src/lib/input/beitragszahlungsweise.ts +++ b/shared/validations/src/lib/input/beitragszahlungsweise.ts @@ -11,7 +11,6 @@ export const BeitragszahlungsweiseSchema = z.enum([ BeitragszahlungsweiseEinmalbeitragSchema.value, BeitragszahlungsweiseMonatlicheBeiträgeSchema.value, ]); -export type Beitragszahlungsweise = z.infer; export const beitragszahlungsweiseOpts: NxDropdownOption[] = Object.entries( BeitragszahlungsweiseSchema.enum diff --git a/shared/validations/src/lib/input/berechnung-der-laufzeit.ts b/shared/validations/src/lib/input/berechnung-der-laufzeit.ts index 5a646c1..f11d149 100644 --- a/shared/validations/src/lib/input/berechnung-der-laufzeit.ts +++ b/shared/validations/src/lib/input/berechnung-der-laufzeit.ts @@ -11,7 +11,6 @@ export const BerechnungDerLaufzeitSchema = z.enum([ BerechnungDerLaufzeitAlterBeiRentenbeginnSchema.value, BerechnungDerLaufzeitAufschubdauerSchema.value, ]); -export type BerechnungDerLaufzeit = z.infer; export const berechnungDerLaufzeitOpts: NxDropdownOption[] = Object.entries( BerechnungDerLaufzeitSchema.enum diff --git a/shared/validations/src/lib/input/leistungsvorgabe.ts b/shared/validations/src/lib/input/leistungsvorgabe.ts index 3044f2a..763f40e 100644 --- a/shared/validations/src/lib/input/leistungsvorgabe.ts +++ b/shared/validations/src/lib/input/leistungsvorgabe.ts @@ -19,7 +19,6 @@ export const LeistungsvorgabeSchema = z.enum([ LeistungsvorgabeGesamtkapitalSchema.value, LeistungsvorgabeGesamtrenteSchema.value, ]); -export type Leistungsvorgabe = z.infer; export const leistungsVorgabeOpts: NxDropdownOption[] = Object.entries( LeistungsvorgabeSchema.enum diff --git a/shared/validations/src/lib/input/rentenzahlungsweise.ts b/shared/validations/src/lib/input/rentenzahlungsweise.ts index 97fcb02..8ba0c60 100644 --- a/shared/validations/src/lib/input/rentenzahlungsweise.ts +++ b/shared/validations/src/lib/input/rentenzahlungsweise.ts @@ -17,7 +17,6 @@ export const RentenzahlungsweiseSchema = z.enum([ RentenzahlungsweiseHalbjaehrlichSchema.value, RentenzahlungsweiseJaehrlichSchema.value, ]); -export type Rentenzahlungsweise = z.infer; export const rentenzahlungsweiseOpts: NxDropdownOption[] = Object.entries( RentenzahlungsweiseSchema.enum diff --git a/tsconfig.base.json b/tsconfig.base.json index 6c9434f..e1931a2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,8 +15,13 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@target/date-picker-lib": ["frontend/libs/date-picker-lib/src/index.ts"], + "@target/error-box-lib": ["frontend/libs/error-box-lib/src/index.ts"], "@target/input-lib": ["frontend/libs/input-lib/src/index.ts"], "@target/interfaces": ["shared/interfaces/src/index.ts"], + "@target/quote-resolver-lib": [ + "frontend/libs/quote-resolver-lib/src/index.ts" + ], "@target/ui-lib": ["frontend/libs/ui-lib/src/index.ts"], "@target/validations": ["shared/validations/src/index.ts"], "@target/view-lib": ["frontend/libs/view-lib/src/index.ts"] From 8e1c534c7c416dea3130e21310c0e1ccd4a7de7e Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Thu, 10 Apr 2025 18:47:12 +0200 Subject: [PATCH 24/26] Code Cleanup: move code classes to new libs --- .../__tests__/input-lib.component.spec.ts | 4 +- .../src/lib/input-lib/input-lib.component.ts | 8 +- .../store/__tests__/input.store.spec.ts | 106 ------------------ frontend/libs/input-store-lib/README.md | 7 ++ .../libs/input-store-lib/eslint.config.cjs | 34 ++++++ frontend/libs/input-store-lib/jest.config.ts | 21 ++++ frontend/libs/input-store-lib/project.json | 20 ++++ frontend/libs/input-store-lib/src/index.ts | 2 + .../src/lib/input-store-lib}/input.store.ts | 4 +- .../interface}/input.store.interfaces.ts | 6 +- .../libs/input-store-lib/src/test-setup.ts | 6 + frontend/libs/input-store-lib/tsconfig.json | 28 +++++ .../libs/input-store-lib/tsconfig.lib.json | 17 +++ .../libs/input-store-lib/tsconfig.spec.json | 16 +++ frontend/libs/quota-store-lib/README.md | 7 ++ .../libs/quota-store-lib/eslint.config.cjs | 34 ++++++ frontend/libs/quota-store-lib/jest.config.ts | 21 ++++ frontend/libs/quota-store-lib/project.json | 20 ++++ frontend/libs/quota-store-lib/src/index.ts | 1 + .../__tests__/quote.store.spec.ts | 4 +- .../interface}/quote.store.interface.ts | 0 .../src/lib/quota-store-lib}/quote.store.ts | 4 +- .../libs/quota-store-lib/src/test-setup.ts | 6 + frontend/libs/quota-store-lib/tsconfig.json | 28 +++++ .../libs/quota-store-lib/tsconfig.lib.json | 17 +++ .../libs/quota-store-lib/tsconfig.spec.json | 16 +++ .../quote-resolver-lib/quote-view.resolver.ts | 4 +- frontend/libs/service-lib/README.md | 7 ++ frontend/libs/service-lib/eslint.config.cjs | 34 ++++++ frontend/libs/service-lib/jest.config.ts | 21 ++++ frontend/libs/service-lib/project.json | 20 ++++ frontend/libs/service-lib/src/index.ts | 1 + .../__tests__/quote.service.spec.ts | 0 .../src/lib/service-lib}/quote.service.ts | 0 frontend/libs/service-lib/src/test-setup.ts | 6 + frontend/libs/service-lib/tsconfig.json | 28 +++++ frontend/libs/service-lib/tsconfig.lib.json | 17 +++ frontend/libs/service-lib/tsconfig.spec.json | 16 +++ frontend/libs/view-lib/src/index.ts | 1 - .../__tests__/view-lib.component.spec.ts | 2 +- .../src/lib/view-lib/view-lib.component.ts | 3 +- tsconfig.base.json | 3 + 42 files changed, 473 insertions(+), 127 deletions(-) delete mode 100644 frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts create mode 100644 frontend/libs/input-store-lib/README.md create mode 100644 frontend/libs/input-store-lib/eslint.config.cjs create mode 100644 frontend/libs/input-store-lib/jest.config.ts create mode 100644 frontend/libs/input-store-lib/project.json create mode 100644 frontend/libs/input-store-lib/src/index.ts rename frontend/libs/{input-lib/src/lib/input-lib/store => input-store-lib/src/lib/input-store-lib}/input.store.ts (97%) rename frontend/libs/{input-lib/src/lib/input-lib/store => input-store-lib/src/lib/input-store-lib/interface}/input.store.interfaces.ts (85%) create mode 100644 frontend/libs/input-store-lib/src/test-setup.ts create mode 100644 frontend/libs/input-store-lib/tsconfig.json create mode 100644 frontend/libs/input-store-lib/tsconfig.lib.json create mode 100644 frontend/libs/input-store-lib/tsconfig.spec.json create mode 100644 frontend/libs/quota-store-lib/README.md create mode 100644 frontend/libs/quota-store-lib/eslint.config.cjs create mode 100644 frontend/libs/quota-store-lib/jest.config.ts create mode 100644 frontend/libs/quota-store-lib/project.json create mode 100644 frontend/libs/quota-store-lib/src/index.ts rename frontend/libs/{view-lib/src/lib/view-lib => quota-store-lib/src/lib/quota-store-lib}/__tests__/quote.store.spec.ts (90%) rename frontend/libs/{view-lib/src/lib/view-lib/store => quota-store-lib/src/lib/quota-store-lib/interface}/quote.store.interface.ts (100%) rename frontend/libs/{view-lib/src/lib/view-lib/store => quota-store-lib/src/lib/quota-store-lib}/quote.store.ts (85%) create mode 100644 frontend/libs/quota-store-lib/src/test-setup.ts create mode 100644 frontend/libs/quota-store-lib/tsconfig.json create mode 100644 frontend/libs/quota-store-lib/tsconfig.lib.json create mode 100644 frontend/libs/quota-store-lib/tsconfig.spec.json create mode 100644 frontend/libs/service-lib/README.md create mode 100644 frontend/libs/service-lib/eslint.config.cjs create mode 100644 frontend/libs/service-lib/jest.config.ts create mode 100644 frontend/libs/service-lib/project.json create mode 100644 frontend/libs/service-lib/src/index.ts rename frontend/libs/{input-lib/src/lib/input-lib/store/services => service-lib/src/lib/service-lib}/__tests__/quote.service.spec.ts (100%) rename frontend/libs/{input-lib/src/lib/input-lib/store/services => service-lib/src/lib/service-lib}/quote.service.ts (100%) create mode 100644 frontend/libs/service-lib/src/test-setup.ts create mode 100644 frontend/libs/service-lib/tsconfig.json create mode 100644 frontend/libs/service-lib/tsconfig.lib.json create mode 100644 frontend/libs/service-lib/tsconfig.spec.json diff --git a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts index a1e81e4..a30c689 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts @@ -3,10 +3,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; +import { InputStore } from '@target/input-store-lib'; +import { QuoteService } from '@target/service-lib'; import { InputLibComponent } from '../../../index'; -import { InputStore } from '../store/input.store'; -import { QuoteService } from '../store/services/quote.service'; describe('InputLibComponent', () => { let component: InputLibComponent; diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index c7c6f75..472ca33 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -15,6 +15,9 @@ import { import { NxInputModule } from '@aposin/ng-aquila/input'; import { NxIsoDateModule } from '@aposin/ng-aquila/iso-date-adapter'; import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; +import { DatepickerComponent } from '@target/date-picker-lib'; +import { ErrorBoxComponent } from '@target/error-box-lib'; +import { Input, InputStore } from '@target/input-store-lib'; import { ApiErrorResponse } from '@target/interfaces'; import { beitragszahlungsweiseOpts, @@ -24,11 +27,6 @@ import { } from '@target/validations'; import { debounceTime } from 'rxjs'; -import { DatepickerComponent } from '../../../../date-picker-lib/src/lib/date-picker-lib/datepicker.component'; -import { InputStore } from './store/input.store'; -import { Input } from './store/input.store.interfaces'; -import { ErrorBoxComponent } from '../../../../error-box-lib/src/lib/error-box-lib/error-box.component'; - @Component({ selector: 'lib-input-lib', standalone: true, diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts deleted file mode 100644 index 7c15d5a..0000000 --- a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { fakerEN } from '@faker-js/faker'; -import { QuoteCreateResponseDto } from '@target/interfaces'; -import { InputDtoSchema } from '@target/validations'; -import { of, throwError } from 'rxjs'; - -import { InputStore } from '../input.store'; -import { QuoteService } from '../services/quote.service'; - -jest.mock('@target/validations', () => ({ - InputDtoSchema: { - safeParseAsync: jest.fn(), - }, -})); - -describe('InputStore', () => { - let store: any; - let quoteService: jest.Mocked; - - const quoteCreateResponseDtoMock: QuoteCreateResponseDto = { - id: fakerEN.string.alpha({ length: 15 }), - }; - - beforeEach(() => { - quoteService = { - calculateQuote: jest.fn(), - } as unknown as jest.Mocked; - - TestBed.configureTestingModule({ - providers: [{ provide: QuoteService, useValue: quoteService }], - }); - - store = TestBed.inject(InputStore); - }); - - describe('updateInputs', () => { - it('should update state when validation succeeds', async () => { - const input = [{ key: 'beitrag', value: 2000 }]; - - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ - success: true, - }); - - await (store as any).updateInputs(input); - - expect(store.uiState().beitrag.value).toBe(2000); - expect(store.uiState().beitrag.valid).toBe(true); - expect(store.uiState().beitrag.error).toBeNull(); - }); - - it('should update state with validation errors when validation fails', async () => { - const input = [{ key: 'beitrag', value: -1 }]; - - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ - success: false, - error: { - errors: [{ path: ['beitrag'], message: 'Beitrag must be positive' }], - }, - }); - - await (store as any).updateInputs(input); - - expect(store.uiState().beitrag.value).toBe(-1); - expect(store.uiState().beitrag.valid).toBe(false); - expect(store.uiState().beitrag.error).toBe('Beitrag must be positive'); - }); - }); - - describe('calculate', () => { - it('should update quote when calculation succeeds', async () => { - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ - success: true, - }); - quoteService.calculateQuote.mockReturnValue( - of(quoteCreateResponseDtoMock) - ); - - await (store as any).calculate(); - - expect(quoteService.calculateQuote).toHaveBeenCalled(); - }); - - it('should handle validation failure', async () => { - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ - success: false, - }); - - await expect((store as any).calculate()).rejects.toThrowError( - 'Invalid input' - ); - }); - - it('should handle API errors', async () => { - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ - success: true, - }); - const error = new Error('Invalid input'); - - quoteService.calculateQuote.mockReturnValue(throwError(() => error)); - - await expect((store as any).calculate()).rejects.toThrow(error); - - expect(quoteService.calculateQuote).toHaveBeenCalled(); - }); - }); -}); diff --git a/frontend/libs/input-store-lib/README.md b/frontend/libs/input-store-lib/README.md new file mode 100644 index 0000000..22c9c52 --- /dev/null +++ b/frontend/libs/input-store-lib/README.md @@ -0,0 +1,7 @@ +# input-store-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test input-store-lib` to execute the unit tests. diff --git a/frontend/libs/input-store-lib/eslint.config.cjs b/frontend/libs/input-store-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/input-store-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/input-store-lib/jest.config.ts b/frontend/libs/input-store-lib/jest.config.ts new file mode 100644 index 0000000..1f11ef3 --- /dev/null +++ b/frontend/libs/input-store-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'input-store-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/input-store-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/input-store-lib/project.json b/frontend/libs/input-store-lib/project.json new file mode 100644 index 0000000..49d1e56 --- /dev/null +++ b/frontend/libs/input-store-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "input-store-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/input-store-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["store"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/input-store-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/input-store-lib/src/index.ts b/frontend/libs/input-store-lib/src/index.ts new file mode 100644 index 0000000..a66ae1f --- /dev/null +++ b/frontend/libs/input-store-lib/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/input-store-lib/input.store'; +export * from './lib/input-store-lib/interface/input.store.interfaces'; diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts b/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts similarity index 97% rename from frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts rename to frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts index 926187a..0c33e05 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts +++ b/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts @@ -1,11 +1,11 @@ import { inject } from '@angular/core'; import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; import { QuoteRequestDto } from '@target/interfaces'; +import { QuoteService } from '@target/service-lib'; import { InputDtoSchema } from '@target/validations'; import { lastValueFrom } from 'rxjs'; -import { Input, InputState } from './input.store.interfaces'; -import { QuoteService } from './services/quote.service'; +import { Input, InputState } from './interface/input.store.interfaces'; const initialState: InputState = { geburtsdatum: { value: '', valid: true, error: null }, diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts b/frontend/libs/input-store-lib/src/lib/input-store-lib/interface/input.store.interfaces.ts similarity index 85% rename from frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts rename to frontend/libs/input-store-lib/src/lib/input-store-lib/interface/input.store.interfaces.ts index 7613430..d391a09 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts +++ b/frontend/libs/input-store-lib/src/lib/input-store-lib/interface/input.store.interfaces.ts @@ -4,7 +4,7 @@ interface InputField { error: string | null; } -export interface InputState { +interface InputState { geburtsdatum: InputField; leistungsVorgabe: InputField; beitrag: InputField; @@ -14,7 +14,9 @@ export interface InputState { rentenzahlungsweise: InputField; } -export interface Input { +interface Input { key: keyof InputState; value: string | number; } + +export { Input,InputState }; diff --git a/frontend/libs/input-store-lib/src/test-setup.ts b/frontend/libs/input-store-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/input-store-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/input-store-lib/tsconfig.json b/frontend/libs/input-store-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/input-store-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/input-store-lib/tsconfig.lib.json b/frontend/libs/input-store-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/input-store-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/input-store-lib/tsconfig.spec.json b/frontend/libs/input-store-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/input-store-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/quota-store-lib/README.md b/frontend/libs/quota-store-lib/README.md new file mode 100644 index 0000000..a3fc7b6 --- /dev/null +++ b/frontend/libs/quota-store-lib/README.md @@ -0,0 +1,7 @@ +# quota-store-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test quota-store-lib` to execute the unit tests. diff --git a/frontend/libs/quota-store-lib/eslint.config.cjs b/frontend/libs/quota-store-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/quota-store-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/quota-store-lib/jest.config.ts b/frontend/libs/quota-store-lib/jest.config.ts new file mode 100644 index 0000000..c974be1 --- /dev/null +++ b/frontend/libs/quota-store-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'quota-store-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/quota-store-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/quota-store-lib/project.json b/frontend/libs/quota-store-lib/project.json new file mode 100644 index 0000000..d3a6a35 --- /dev/null +++ b/frontend/libs/quota-store-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "quota-store-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/quota-store-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["store"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/quota-store-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/quota-store-lib/src/index.ts b/frontend/libs/quota-store-lib/src/index.ts new file mode 100644 index 0000000..a2bc78d --- /dev/null +++ b/frontend/libs/quota-store-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/quota-store-lib/quote.store'; diff --git a/frontend/libs/view-lib/src/lib/view-lib/__tests__/quote.store.spec.ts b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/__tests__/quote.store.spec.ts similarity index 90% rename from frontend/libs/view-lib/src/lib/view-lib/__tests__/quote.store.spec.ts rename to frontend/libs/quota-store-lib/src/lib/quota-store-lib/__tests__/quote.store.spec.ts index 60213af..248dc07 100644 --- a/frontend/libs/view-lib/src/lib/view-lib/__tests__/quote.store.spec.ts +++ b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/__tests__/quote.store.spec.ts @@ -1,10 +1,10 @@ import { TestBed } from '@angular/core/testing'; import { fakerEN } from '@faker-js/faker'; import { QuoteResponseDto } from '@target/interfaces'; +import { QuoteService } from '@target/service-lib'; import { of } from 'rxjs'; -import { QuoteService } from '../../../../../input-lib/src/lib/input-lib/store/services/quote.service'; -import { QuoteStore } from '../store/quote.store'; +import { QuoteStore } from '../quote.store'; describe('QuoteStore', () => { let store: any; diff --git a/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.interface.ts b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/interface/quote.store.interface.ts similarity index 100% rename from frontend/libs/view-lib/src/lib/view-lib/store/quote.store.interface.ts rename to frontend/libs/quota-store-lib/src/lib/quota-store-lib/interface/quote.store.interface.ts diff --git a/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.ts b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/quote.store.ts similarity index 85% rename from frontend/libs/view-lib/src/lib/view-lib/store/quote.store.ts rename to frontend/libs/quota-store-lib/src/lib/quota-store-lib/quote.store.ts index c421b96..7631ea4 100644 --- a/frontend/libs/view-lib/src/lib/view-lib/store/quote.store.ts +++ b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/quote.store.ts @@ -1,9 +1,9 @@ import { inject } from '@angular/core'; import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { QuoteService } from '@target/service-lib'; import { lastValueFrom } from 'rxjs'; -import { QuoteService } from '../../../../../input-lib/src/lib/input-lib/store/services/quote.service'; -import { QuoteState } from './quote.store.interface'; +import { QuoteState } from './interface/quote.store.interface'; const initialState: QuoteState = { basisdaten: { diff --git a/frontend/libs/quota-store-lib/src/test-setup.ts b/frontend/libs/quota-store-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/quota-store-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/quota-store-lib/tsconfig.json b/frontend/libs/quota-store-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/quota-store-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/quota-store-lib/tsconfig.lib.json b/frontend/libs/quota-store-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/quota-store-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/quota-store-lib/tsconfig.spec.json b/frontend/libs/quota-store-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/quota-store-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts index 354f9ae..0dedf84 100644 --- a/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts +++ b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts @@ -1,8 +1,8 @@ import { inject } from '@angular/core'; import { ResolveFn } from '@angular/router'; -import { QuoteStore } from '@target/view-lib'; +import { QuoteStore } from '@target/quota-store-lib'; -export const quoteViewResolver: ResolveFn = (route, state) => { +export const quoteViewResolver: ResolveFn = (route, _) => { const quoteStore = inject(QuoteStore); return quoteStore.fetchQuote(route.params['id']); diff --git a/frontend/libs/service-lib/README.md b/frontend/libs/service-lib/README.md new file mode 100644 index 0000000..830b315 --- /dev/null +++ b/frontend/libs/service-lib/README.md @@ -0,0 +1,7 @@ +# service-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test service-lib` to execute the unit tests. diff --git a/frontend/libs/service-lib/eslint.config.cjs b/frontend/libs/service-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/service-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/service-lib/jest.config.ts b/frontend/libs/service-lib/jest.config.ts new file mode 100644 index 0000000..b7ba779 --- /dev/null +++ b/frontend/libs/service-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'service-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/service-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/service-lib/project.json b/frontend/libs/service-lib/project.json new file mode 100644 index 0000000..41231cb --- /dev/null +++ b/frontend/libs/service-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "service-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/service-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["service"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/service-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/service-lib/src/index.ts b/frontend/libs/service-lib/src/index.ts new file mode 100644 index 0000000..2852973 --- /dev/null +++ b/frontend/libs/service-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/service-lib/quote.service'; diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts b/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts similarity index 100% rename from frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts rename to frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts b/frontend/libs/service-lib/src/lib/service-lib/quote.service.ts similarity index 100% rename from frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts rename to frontend/libs/service-lib/src/lib/service-lib/quote.service.ts diff --git a/frontend/libs/service-lib/src/test-setup.ts b/frontend/libs/service-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/service-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/service-lib/tsconfig.json b/frontend/libs/service-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/service-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/service-lib/tsconfig.lib.json b/frontend/libs/service-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/service-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/service-lib/tsconfig.spec.json b/frontend/libs/service-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/service-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/view-lib/src/index.ts b/frontend/libs/view-lib/src/index.ts index e2e3022..86fd00c 100644 --- a/frontend/libs/view-lib/src/index.ts +++ b/frontend/libs/view-lib/src/index.ts @@ -1,2 +1 @@ -export * from './lib/view-lib/store/quote.store'; export * from './lib/view-lib/view-lib.component'; diff --git a/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts index 0fc7ea7..102d93f 100644 --- a/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts +++ b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { QuoteStore } from '@target/quota-store-lib'; -import { QuoteStore } from '../store/quote.store'; import { ViewLibComponent } from '../view-lib.component'; describe('ViewLibComponent', () => { diff --git a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts index 80ca935..079248b 100644 --- a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts +++ b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts @@ -6,8 +6,7 @@ import { NxLayoutComponent, NxRowComponent, } from '@aposin/ng-aquila/grid'; - -import { QuoteStore } from './store/quote.store'; +import { QuoteStore } from '@target/quota-store-lib'; @Component({ selector: 'lib-view-lib', diff --git a/tsconfig.base.json b/tsconfig.base.json index e1931a2..13892ce 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,10 +18,13 @@ "@target/date-picker-lib": ["frontend/libs/date-picker-lib/src/index.ts"], "@target/error-box-lib": ["frontend/libs/error-box-lib/src/index.ts"], "@target/input-lib": ["frontend/libs/input-lib/src/index.ts"], + "@target/input-store-lib": ["frontend/libs/input-store-lib/src/index.ts"], "@target/interfaces": ["shared/interfaces/src/index.ts"], + "@target/quota-store-lib": ["frontend/libs/quota-store-lib/src/index.ts"], "@target/quote-resolver-lib": [ "frontend/libs/quote-resolver-lib/src/index.ts" ], + "@target/service-lib": ["frontend/libs/service-lib/src/index.ts"], "@target/ui-lib": ["frontend/libs/ui-lib/src/index.ts"], "@target/validations": ["shared/validations/src/index.ts"], "@target/view-lib": ["frontend/libs/view-lib/src/index.ts"] From 04b033016802779e991258e9616b07fb00003b60 Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Thu, 10 Apr 2025 18:58:28 +0200 Subject: [PATCH 25/26] Fix: fix validation message --- shared/validations/src/lib/input/input.validations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/validations/src/lib/input/input.validations.ts b/shared/validations/src/lib/input/input.validations.ts index 04892c8..88f770a 100644 --- a/shared/validations/src/lib/input/input.validations.ts +++ b/shared/validations/src/lib/input/input.validations.ts @@ -17,7 +17,7 @@ export const InputDtoSchema = z.object({ laufzeit: z .number() .min(1, 'Die Laufzeit muss mindestens 1 Jahr betragen') - .max(100, 'Die Laufzeit darf höchstens 40 Jahre betragen'), + .max(40, 'Die Laufzeit darf höchstens 40 Jahre betragen'), beitragszahlungsweise: BeitragszahlungsweiseSchema.nullish(), rentenzahlungsweise: RentenzahlungsweiseSchema.nullish(), }); From c5b78d8828f7e9a3d90ca06fe871eacf6e4bfaeb Mon Sep 17 00:00:00 2001 From: Julio Fernandez Date: Fri, 11 Apr 2025 08:16:43 +0200 Subject: [PATCH 26/26] Fix: add missing service test --- .../__tests__/input.store.spec.ts | 115 ++++++++++++++++++ .../src/lib/input-store-lib/input.store.ts | 12 +- .../__tests__/quote.service.spec.ts | 60 +++++---- .../src/lib/service-lib/quote.service.ts | 12 +- shared/interfaces/src/lib/quote.interfaces.ts | 4 - 5 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 frontend/libs/input-store-lib/src/lib/input-store-lib/__tests__/input.store.spec.ts diff --git a/frontend/libs/input-store-lib/src/lib/input-store-lib/__tests__/input.store.spec.ts b/frontend/libs/input-store-lib/src/lib/input-store-lib/__tests__/input.store.spec.ts new file mode 100644 index 0000000..0082b9f --- /dev/null +++ b/frontend/libs/input-store-lib/src/lib/input-store-lib/__tests__/input.store.spec.ts @@ -0,0 +1,115 @@ +import { TestBed } from '@angular/core/testing'; +import { fakerEN } from '@faker-js/faker'; +import { QuoteResponseDto } from '@target/interfaces'; +import { QuoteService } from '@target/service-lib'; +import { InputDtoSchema } from '@target/validations'; +import { of } from 'rxjs'; + +import { InputStore } from '../input.store'; + +jest.mock('@target/validations', () => ({ + InputDtoSchema: { + safeParseAsync: jest.fn(), + }, +})); + +describe('InputStore', () => { + let store: any; + let quoteService: QuoteService; + + const mockQuoteResponse: QuoteResponseDto = { + id: fakerEN.string.alpha({ length: 15 }), + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 50000, + einmaligesGarantiekapital: 25000, + todesfallleistungAbAltersrentenbezug: 40000, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: QuoteService, + useValue: { calculateQuote: jest.fn(), fetchQuote: jest.fn() }, + }, + ], + }); + + store = TestBed.inject(InputStore); + quoteService = TestBed.inject(QuoteService); + }); + + describe('updateInputs', () => { + it('should update state when validation succeeds', async () => { + const input = { key: 'beitrag', value: 2000 }; + + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: true, + }); + + await (store as any).updateInputs([input]); + + expect(store.uiState().beitrag.value).toBe(2000); + expect(store.uiState().beitrag.valid).toBe(true); + expect(store.uiState().beitrag.error).toBeNull(); + }); + + it('should update state with validation errors when validation fails', async () => { + const input = { key: 'beitrag', value: -1 }; + + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: false, + error: { + errors: [{ path: ['beitrag'], message: 'Beitrag must be positive' }], + }, + }); + + await (store as any).updateInputs([input]); + + expect(store.uiState().beitrag.value).toBe(-1); + expect(store.uiState().beitrag.valid).toBe(false); + expect(store.uiState().beitrag.error).toBe('Beitrag must be positive'); + }); + }); + + describe('calculate', () => { + it('should update quote when calculation succeeds', async () => { + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: true, + }); + + jest + .spyOn(quoteService, 'calculateQuote') + .mockReturnValue(of(mockQuoteResponse)); + + await (store as any).calculate(); + + expect(quoteService.calculateQuote).toHaveBeenCalled(); + }); + + it('should handle validation failure', async () => { + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: false, + }); + + await expect((store as any).calculate()).rejects.toThrow( + new Error('Invalid input') + ); + + expect(quoteService.calculateQuote).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts b/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts index 0c33e05..e6531ce 100644 --- a/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts +++ b/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts @@ -118,11 +118,13 @@ export const InputStore = signalStore( throw new Error('Invalid input'); } - return ( - await lastValueFrom( - quoteService.calculateQuote(quoteDto as QuoteRequestDto) - ) - ).id; + return ( + ( + await lastValueFrom( + quoteService.calculateQuote(quoteDto as QuoteRequestDto) + ) + ).id + ); }, })) ); diff --git a/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts b/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts index 92d5f27..d9f60ce 100644 --- a/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts +++ b/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts @@ -1,5 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; +import { fakerEN } from '@faker-js/faker'; import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; import { firstValueFrom, of, throwError } from 'rxjs'; @@ -7,10 +8,30 @@ import { QuoteService } from '../quote.service'; describe('QuoteService', () => { let service: QuoteService; - let httpClient: { post: jest.Mock }; + let httpClient: { post: jest.Mock; get: jest.Mock }; + + const mockResponse: QuoteResponseDto = { + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 50000, + einmaligesGarantiekapital: 25000, + todesfallleistungAbAltersrentenbezug: 40000, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '1,5%', + }, + }; beforeEach(() => { - httpClient = { post: jest.fn() }; + httpClient = { post: jest.fn(), get: jest.fn() }; TestBed.configureTestingModule({ providers: [QuoteService, { provide: HttpClient, useValue: httpClient }], }); @@ -34,26 +55,6 @@ describe('QuoteService', () => { beitragszahlungsweise: 'Monatliche Beiträge', }; - const mockResponse: QuoteResponseDto = { - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2024-01-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 10, - }, - leistungsmerkmale: { - garantierteMindestrente: 50000, - einmaligesGarantiekapital: 25000, - todesfallleistungAbAltersrentenbezug: 40000, - }, - beitrag: { - einmalbeitrag: 0, - beitragsdynamik: '1,5%', - }, - }; - httpClient.post.mockReturnValue(of(mockResponse)); const response = await firstValueFrom( @@ -85,4 +86,19 @@ describe('QuoteService', () => { expect(httpClient.post).toHaveBeenCalledWith('/api/quote', mockRequest); }); }); + + describe('fetchQuote', () => { + it('should fetch quote data', async () => { + const id = fakerEN.string.alpha({ length: 15 }); + + const mockTestResponse: QuoteResponseDto = { ...mockResponse, id }; + + httpClient.get.mockReturnValue(of(mockTestResponse)); + + const response = await firstValueFrom(service.fetchQuote(id)); + + expect(response).toEqual(mockTestResponse); + expect(httpClient.get).toHaveBeenCalledWith(`/api/quote/${id}`); + }); + }); }); diff --git a/frontend/libs/service-lib/src/lib/service-lib/quote.service.ts b/frontend/libs/service-lib/src/lib/service-lib/quote.service.ts index c596595..a722dda 100644 --- a/frontend/libs/service-lib/src/lib/service-lib/quote.service.ts +++ b/frontend/libs/service-lib/src/lib/service-lib/quote.service.ts @@ -1,20 +1,14 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { - QuoteCreateResponseDto, - QuoteRequestDto, - QuoteResponseDto, -} from '@target/interfaces'; +import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class QuoteService { private readonly http = inject(HttpClient); - calculateQuote( - quoteDto: QuoteRequestDto - ): Observable { - return this.http.post('/api/quote', quoteDto); + calculateQuote(quoteDto: QuoteRequestDto): Observable { + return this.http.post('/api/quote', quoteDto); } fetchQuote(quoteId: string): Observable { diff --git a/shared/interfaces/src/lib/quote.interfaces.ts b/shared/interfaces/src/lib/quote.interfaces.ts index 7349b0e..1839792 100644 --- a/shared/interfaces/src/lib/quote.interfaces.ts +++ b/shared/interfaces/src/lib/quote.interfaces.ts @@ -9,10 +9,6 @@ export interface QuoteResponseDto { beitrag: IQuoteBeitrag; } -export interface QuoteCreateResponseDto { - id: string; -} - interface IQuoteBasisdaten { geburtsdatum: string; versicherungsbeginn: string;