diff --git a/package-lock.json b/package-lock.json index 73f1ab5..503dd5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,40 +10,42 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/axios": "^3.0.3", - "@nestjs/common": "^10.0.0", + "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", - "@nestjs/core": "^10.0.0", - "@nestjs/platform-express": "^10.0.0", + "@nestjs/core": "^10.4.1", + "@nestjs/platform-express": "^10.4.1", "@nestjs/schedule": "^4.1.0", "@prisma/client": "^5.19.1", "axios": "^1.7.7", + "lodash": "^4.17.21", "luxon": "^3.5.0", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.5.0", - "prettier": "^3.0.0", + "@nestjs/cli": "^10.4.5", + "@nestjs/schematics": "^10.1.4", + "@nestjs/testing": "^10.4.1", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.7", + "@types/node": "^20.16.5", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.5.0", + "@typescript-eslint/parser": "^8.5.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "jest": "^29.7.0", + "prettier": "^3.3.3", "prisma": "^5.19.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", - "ts-jest": "^29.1.0", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.6.2" } }, "node_modules/@ampproject/remapping": { @@ -1572,10 +1574,11 @@ } }, "node_modules/@nestjs/cli": { - "version": "10.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", - "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz", + "integrity": "sha512-FP7Rh13u8aJbHe+zZ7hM0CC4785g9Pw4lz4r2TTgRtf0zTxSWMkJaPEwyjX8SK9oWK2GsYxl+fKpwVZNbmnj9A==", "dev": true, + "license": "MIT", "dependencies": { "@angular-devkit/core": "17.3.8", "@angular-devkit/schematics": "17.3.8", @@ -1594,7 +1597,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.93.0", + "webpack": "5.94.0", "webpack-node-externals": "3.0.0" }, "bin": { @@ -1616,28 +1619,6 @@ } } }, - "node_modules/@nestjs/cli/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nestjs/cli/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@nestjs/cli/node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -1651,53 +1632,6 @@ "node": ">=14.17" } }, - "node_modules/@nestjs/cli/node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, "node_modules/@nestjs/common": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", @@ -2116,21 +2050,13 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2216,6 +2142,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/luxon": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", @@ -2235,10 +2168,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.16.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", - "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } @@ -2320,16 +2254,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", - "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz", + "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/type-utils": "8.2.0", - "@typescript-eslint/utils": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/type-utils": "8.5.0", + "@typescript-eslint/utils": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2353,15 +2288,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", - "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz", + "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", "debug": "^4.3.4" }, "engines": { @@ -2381,13 +2317,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", - "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", + "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0" + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2398,13 +2335,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", - "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz", + "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/typescript-estree": "8.5.0", + "@typescript-eslint/utils": "8.5.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2422,10 +2360,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", - "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", + "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2435,15 +2374,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", - "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", + "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -2463,15 +2403,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", - "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0" + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2485,12 +2426,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", - "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", + "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/types": "8.5.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2864,15 +2806,6 @@ "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -3852,18 +3785,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4442,6 +4363,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4912,26 +4834,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -6290,7 +6192,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6415,6 +6318,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -8427,10 +8331,11 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8607,7 +8512,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, - "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -8672,7 +8576,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -8686,7 +8589,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } diff --git a/package.json b/package.json index d161c8e..7a614f7 100644 --- a/package.json +++ b/package.json @@ -21,40 +21,42 @@ }, "dependencies": { "@nestjs/axios": "^3.0.3", - "@nestjs/common": "^10.0.0", + "@nestjs/common": "^10.4.1", "@nestjs/config": "^3.2.3", - "@nestjs/core": "^10.0.0", - "@nestjs/platform-express": "^10.0.0", + "@nestjs/core": "^10.4.1", + "@nestjs/platform-express": "^10.4.1", "@nestjs/schedule": "^4.1.0", "@prisma/client": "^5.19.1", "axios": "^1.7.7", + "lodash": "^4.17.21", "luxon": "^3.5.0", - "reflect-metadata": "^0.2.0", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.5.0", - "prettier": "^3.0.0", + "@nestjs/cli": "^10.4.5", + "@nestjs/schematics": "^10.1.4", + "@nestjs/testing": "^10.4.1", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.7", + "@types/node": "^20.16.5", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^8.5.0", + "@typescript-eslint/parser": "^8.5.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "jest": "^29.7.0", + "prettier": "^3.3.3", "prisma": "^5.19.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", - "ts-jest": "^29.1.0", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.6.2" }, "jest": { "moduleFileExtensions": [ diff --git a/prisma/sql/getAllocatorBiggestClientDistribution.sql b/prisma/sql/getAllocatorBiggestClientDistribution.sql new file mode 100644 index 0000000..59edf37 --- /dev/null +++ b/prisma/sql/getAllocatorBiggestClientDistribution.sql @@ -0,0 +1,23 @@ +with allocators_with_ratio as ( + select + week, + allocator, + max(sum_of_allocations) / sum(sum_of_allocations) biggest_to_total_ratio + from client_allocator_distribution_weekly + group by + week, + allocator +) +select + week, + 100 * ceil(biggest_to_total_ratio::float8 * 20) / 20 - 5 as "valueFromExclusive", + 100 * ceil(biggest_to_total_ratio::float8 * 20) / 20 as "valueToInclusive", + count(*)::int as count +from allocators_with_ratio +group by + week, + "valueFromExclusive", + "valueToInclusive" +order by + week, + "valueFromExclusive"; \ No newline at end of file diff --git a/prisma/sql/getAllocatorRetrievability.sql b/prisma/sql/getAllocatorRetrievability.sql new file mode 100644 index 0000000..befd747 --- /dev/null +++ b/prisma/sql/getAllocatorRetrievability.sql @@ -0,0 +1,8 @@ +select + ceil(avg_weighted_retrievability_success_rate*20)*5 - 5 as "valueFromExclusive", + ceil(avg_weighted_retrievability_success_rate*20)*5 as "valueToInclusive", + count(*)::int as "count", + week +from allocators_weekly +group by 1, 2, week +order by week; diff --git a/prisma/sql/getProviderBiggestClientDistribution.sql b/prisma/sql/getProviderBiggestClientDistribution.sql new file mode 100644 index 0000000..a6c1745 --- /dev/null +++ b/prisma/sql/getProviderBiggestClientDistribution.sql @@ -0,0 +1,12 @@ +with providers_with_ratio as (select provider, + max(total_deal_size) / sum(total_deal_size) biggest_to_total_ratio, + week + from client_provider_distribution_weekly + group by provider, week) +select 100 * ceil(biggest_to_total_ratio::float8 * 20) / 20 - 5 as "valueFromExclusive", + 100 * ceil(biggest_to_total_ratio::float8 * 20) / 20 as "valueToInclusive", + count(*)::int as "count", + week +from providers_with_ratio +group by "valueFromExclusive", "valueToInclusive", week +order by week, 1; diff --git a/prisma/sql/getProviderClientsWeekly.sql b/prisma/sql/getProviderClientsWeekly.sql new file mode 100644 index 0000000..16e3195 --- /dev/null +++ b/prisma/sql/getProviderClientsWeekly.sql @@ -0,0 +1,11 @@ +with clients_per_provider as (select count(distinct client) as clients_count, + week + from client_provider_distribution_weekly + group by provider, week) +select (clients_count - 1)::float as "valueFromExclusive", + clients_count::float as "valueToInclusive", + week as "week", + count(*)::int as "count" +from clients_per_provider +group by 1, 2, 3 +order by 3, 1; diff --git a/prisma/sql/getProviderRetrievability.sql b/prisma/sql/getProviderRetrievability.sql new file mode 100644 index 0000000..215692d --- /dev/null +++ b/prisma/sql/getProviderRetrievability.sql @@ -0,0 +1,7 @@ +select 100 * ceil(avg_retrievability_success_rate * 20) / 20 - 5 as "valueFromExclusive", + 100 * ceil(avg_retrievability_success_rate * 20) / 20 as "valueToInclusive", + count(*)::int as "count", + week as "week" +from providers_weekly +group by 1, 2, 4 +order by 1; diff --git a/src/aggregation/aggregation-tasks.service.ts b/src/aggregation/aggregation-tasks.service.ts index f64d7d2..ade96e8 100644 --- a/src/aggregation/aggregation-tasks.service.ts +++ b/src/aggregation/aggregation-tasks.service.ts @@ -14,14 +14,23 @@ export class AggregationTasksService { if (!this.aggregationJobInProgress) { this.aggregationJobInProgress = true; - try { - this.logger.debug('Starting Aggregations'); + let success = false; + let executionNumber = 0; + //retry up to 3 times in case of error + while (!success && executionNumber < 3) { + try { + this.logger.debug('Starting Aggregations'); - await this.aggregationService.runAggregations(); + await this.aggregationService.runAggregations(); + success = true; - this.logger.debug('Finished Aggregations'); - } catch (err) { - this.logger.error(`Error during Aggregations job: ${err}`); + this.logger.debug('Finished Aggregations'); + } catch (err) { + this.logger.error( + `Error during Aggregations job, execution ${executionNumber}: ${err}`, + ); + executionNumber++; + } } this.aggregationJobInProgress = false; diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 6206bde..121baf3 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,4 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { AggregationTasksService } from './aggregation/aggregation-tasks.service'; @@ -21,6 +19,11 @@ import { UnifiedVerifiedDealRunner } from './aggregation/runners/unified-verifie import { HttpModule } from '@nestjs/axios'; import { FilSparkService } from './filspark/filspark.service'; import { ProviderRetrievabilityBackfillRunner } from './aggregation/runners/provider-retrievability-backfill.runner'; +import { ProvidersController } from './controller/stats/providers/providers.controller'; +import { ProviderService } from './service/provider/provider.service'; +import { AllocatorsController } from './controller/stats/allocators/allocators.controller'; +import { AllocatorService } from './service/allocator/allocator.service'; +import { HistogramHelper } from './helper/histogram.helper'; @Module({ imports: [ @@ -28,9 +31,8 @@ import { ProviderRetrievabilityBackfillRunner } from './aggregation/runners/prov ScheduleModule.forRoot(), HttpModule.register({ timeout: 5000 }), ], - controllers: [AppController], + controllers: [ProvidersController, AllocatorsController], providers: [ - AppService, AggregationService, AggregationTasksService, PrismaService, @@ -47,6 +49,9 @@ import { ProviderRetrievabilityBackfillRunner } from './aggregation/runners/prov ProviderRetrievabilityBackfillRunner, ProvidersRunner, UnifiedVerifiedDealRunner, + ProviderService, + AllocatorService, + HistogramHelper, { provide: 'AggregationRunner', useFactory: ( diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/controller/stats/allocators/allocators.controller.spec.ts b/src/controller/stats/allocators/allocators.controller.spec.ts new file mode 100644 index 0000000..80387ac --- /dev/null +++ b/src/controller/stats/allocators/allocators.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AllocatorsController } from './allocators.controller'; + +describe('AllocatorsController', () => { + let controller: AllocatorsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AllocatorsController], + }).compile(); + + controller = module.get(AllocatorsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/controller/stats/allocators/allocators.controller.ts b/src/controller/stats/allocators/allocators.controller.ts new file mode 100644 index 0000000..b2802c5 --- /dev/null +++ b/src/controller/stats/allocators/allocators.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get } from '@nestjs/common'; +import { AllocatorService } from '../../../service/allocator/allocator.service'; + +@Controller('stats/allocators') +export class AllocatorsController { + constructor(private readonly allocatorService: AllocatorService) {} + + @Get('retrievability') + async getAllocatorRetrievability() { + return await this.allocatorService.getAllocatorRetrievability(); + } + + @Get('biggest-client-distribution') + async getAllocatorBiggestClientDistribution() { + return await this.allocatorService.getAllocatorBiggestClientDistribution(); + } + + @Get('sps-compliance') + async getAllocatorSpsCompliance() { + return await this.allocatorService.getAllocatorSpsCompliance(); + } +} diff --git a/src/controller/stats/providers/providers.controller.spec.ts b/src/controller/stats/providers/providers.controller.spec.ts new file mode 100644 index 0000000..9b6576b --- /dev/null +++ b/src/controller/stats/providers/providers.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProvidersController } from './providers.controller'; + +describe('ProvidersController', () => { + let controller: ProvidersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ProvidersController], + }).compile(); + + controller = module.get(ProvidersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/controller/stats/providers/providers.controller.ts b/src/controller/stats/providers/providers.controller.ts new file mode 100644 index 0000000..621d91f --- /dev/null +++ b/src/controller/stats/providers/providers.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get } from '@nestjs/common'; +import { ProviderService } from '../../../service/provider/provider.service'; + +@Controller('stats/providers') +export class ProvidersController { + constructor(private readonly providerService: ProviderService) {} + + @Get('clients') + async getProviderClients() { + return await this.providerService.getProviderClients(); + } + + @Get('biggest-client-distribution') + async getProviderBiggestClientDistribution() { + return await this.providerService.getProviderBiggestClientDistribution(); + } + + @Get('retrievability') + async getProviderRetrievability() { + return await this.providerService.getProviderRetrievability(); + } +} diff --git a/src/helper/histogram.helper.ts b/src/helper/histogram.helper.ts new file mode 100644 index 0000000..70b6b63 --- /dev/null +++ b/src/helper/histogram.helper.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { groupBy } from 'lodash'; +import { HistogramWeekDto } from '../types/histogramWeek.dto'; +import { HistogramDto } from '../types/histogram.dto'; +import { HistogramWeekResponseDto } from '../types/histogramWeek.response.dto'; + +@Injectable() +export class HistogramHelper { + async getWeeklyHistogramResult( + results: { + valueFromExclusive: number | null; + valueToInclusive: number | null; + count: number | null; + week: Date; + }[], + totalCount: number, + ): Promise { + const resultsByWeek = groupBy(results, (p) => p.week); + + const histogramWeekDtos: HistogramWeekDto[] = []; + for (const key in resultsByWeek) { + const value = resultsByWeek[key]; + const weekResponses = value.map((r) => { + return new HistogramDto( + r.valueFromExclusive, + r.valueToInclusive, + r.count, + ); + }); + histogramWeekDtos.push( + new HistogramWeekDto( + new Date(key), + weekResponses, + weekResponses.reduce( + (partialSum, response) => partialSum + response.count, + 0, + ), + ), + ); + } + + // calculate missing histogram buckets + const { maxMinSpan, allBucketTopValues } = + this.getAllHistogramBucketTopValues(histogramWeekDtos); + + for (const histogramWeekDto of histogramWeekDtos) { + const missingValues = allBucketTopValues.filter( + (topValue) => + !histogramWeekDto.results.some( + (p) => p.valueToInclusive === topValue, + ), + ); + + if (missingValues.length > 0) { + histogramWeekDto.results.push( + ...missingValues.map((v) => new HistogramDto(v - maxMinSpan, v, 0)), + ); + + histogramWeekDto.results.sort( + (a, b) => a.valueToInclusive - b.valueToInclusive, + ); + } + } + + return new HistogramWeekResponseDto(totalCount, histogramWeekDtos); + } + + private getAllHistogramBucketTopValues( + histogramWeekDtos: HistogramWeekDto[], + ) { + const maxRangeTopValue = Math.max( + ...histogramWeekDtos.flatMap((p) => + p.results.map((r) => r.valueToInclusive), + ), + ); + + const maxHistogramEntry = histogramWeekDtos + .flatMap((p) => p.results) + .find((p) => p.valueToInclusive === maxRangeTopValue); + + const maxMinSpan = + maxHistogramEntry.valueToInclusive - maxHistogramEntry.valueFromExclusive; + + const allBucketTopValues: number[] = []; + for (let i = maxRangeTopValue; i > 0; i -= maxMinSpan) { + allBucketTopValues.push(i); + } + return { maxMinSpan, allBucketTopValues }; + } +} diff --git a/src/main.ts b/src/main.ts index 13cad38..da5451c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors(); await app.listen(3000); } bootstrap(); diff --git a/src/service/allocator/allocator.service.spec.ts b/src/service/allocator/allocator.service.spec.ts new file mode 100644 index 0000000..09e5980 --- /dev/null +++ b/src/service/allocator/allocator.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AllocatorService } from './allocator.service'; + +describe('AllocatorService', () => { + let service: AllocatorService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AllocatorService], + }).compile(); + + service = module.get(AllocatorService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/service/allocator/allocator.service.ts b/src/service/allocator/allocator.service.ts new file mode 100644 index 0000000..02f66be --- /dev/null +++ b/src/service/allocator/allocator.service.ts @@ -0,0 +1,350 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../db/prisma.service'; +import { + getAllocatorBiggestClientDistribution, + getAllocatorRetrievability, +} from '../../../prisma/generated/client/sql'; +import { HistogramHelper } from '../../helper/histogram.helper'; +import { RetrievabilityWeekResponseDto } from '../../types/retrievabilityWeekResponse.dto'; +import { groupBy } from 'lodash'; +import { ProviderComplianceScoreRange } from '../../types/providerComplianceScoreRange'; +import { SpsComplianceWeekResponseDto } from '../../types/spsComplianceWeekResponse.dto'; +import { SpsComplianceWeekDto } from '../../types/spsComplianceWeek.dto'; +import { DateTime } from 'luxon'; + +@Injectable() +export class AllocatorService { + constructor( + private readonly prismaService: PrismaService, + private readonly histogramHelper: HistogramHelper, + ) {} + + async getAllocatorRetrievability() { + const averageSuccessRate = + await this.prismaService.allocators_weekly.aggregate({ + _avg: { + avg_weighted_retrievability_success_rate: true, + }, + where: { + week: DateTime.now() + .toUTC() + .minus({ week: 1 }) + .startOf('week') + .toJSDate(), + }, + }); + + const allocatorCountResult = await this.prismaService.$queryRaw< + [ + { + count: number; + }, + ] + >`select count(distinct allocator)::int + from client_allocator_distribution_weekly`; + + const weeklyHistogramResult = + await this.histogramHelper.getWeeklyHistogramResult( + await this.prismaService.$queryRawTyped(getAllocatorRetrievability()), + allocatorCountResult[0].count, + ); + + return RetrievabilityWeekResponseDto.of( + averageSuccessRate._avg.avg_weighted_retrievability_success_rate * 100, + weeklyHistogramResult, + ); + } + + async getAllocatorBiggestClientDistribution() { + const allocatorCountResult = await this.prismaService.$queryRaw< + [ + { + count: number; + }, + ] + >`select count(distinct allocator)::int + from client_allocator_distribution_weekly`; + + return await this.histogramHelper.getWeeklyHistogramResult( + await this.prismaService.$queryRawTyped( + getAllocatorBiggestClientDistribution(), + ), + allocatorCountResult[0].count, + ); + } + + async getAllocatorSpsCompliance(): Promise { + const weeks = await this.prismaService.providers_weekly + .findMany({ + distinct: ['week'], + select: { + week: true, + }, + }) + .then((r) => r.map((p) => p.week)); + + const allocatorCount = ( + await this.prismaService.allocators_weekly.findMany({ + distinct: ['allocator'], + select: { + allocator: true, + }, + }) + ).length; + + const calculationResults: { + results: { + upToOneCompliantProvidersPercent: number; + twoCompliantProvidersPercent: number; + threeCompliantProvidersPercent: number; + }[]; + week: Date; + }[] = []; + for (const week of weeks) { + const thisWeekAverageRetrievability = + await this.prismaService.providers_weekly.aggregate({ + _avg: { + avg_retrievability_success_rate: true, + }, + where: { + week: week, + }, + }); + + const weekProviders = await this.prismaService.providers_weekly.findMany({ + where: { + week: week, + }, + }); + + const weekProvidersCompliance = weekProviders.map((wp) => { + let complianceScore = 0; + if ( + wp.avg_retrievability_success_rate > + thisWeekAverageRetrievability._avg.avg_retrievability_success_rate + ) + complianceScore++; + if (wp.num_of_clients > 3) complianceScore++; + if ( + wp.biggest_client_total_deal_size * 100n <= + 30n * wp.total_deal_size + ) + complianceScore++; + + return { + provider: wp.provider, + complianceScore: complianceScore, + }; + }); + + const weekAllocatorsWithClients = + await this.prismaService.client_allocator_distribution_weekly.findMany({ + where: { + week: week, + }, + select: { + client: true, + allocator: true, + }, + }); + + const byAllocators = groupBy( + weekAllocatorsWithClients, + (a) => a.allocator, + ); + + const weekResult: { + allocator: string; + upToOneCompliantProvidersPercent: number; + twoCompliantProvidersPercent: number; + threeCompliantProvidersPercent: number; + }[] = []; + for (const allocator in byAllocators) { + const clients = byAllocators[allocator].map((p) => p.client); + + const providers = + await this.prismaService.client_provider_distribution_weekly + .findMany({ + where: { + week: week, + client: { + in: clients, + }, + }, + select: { + provider: true, + }, + }) + .then((r) => r.map((p) => p.provider)); + + weekResult.push({ + allocator: allocator, + upToOneCompliantProvidersPercent: + this.getAllocatorCompliantProvidersPercentage( + weekProvidersCompliance, + providers, + ProviderComplianceScoreRange.UpToOne, + ), + twoCompliantProvidersPercent: + this.getAllocatorCompliantProvidersPercentage( + weekProvidersCompliance, + providers, + ProviderComplianceScoreRange.Two, + ), + threeCompliantProvidersPercent: + this.getAllocatorCompliantProvidersPercentage( + weekProvidersCompliance, + providers, + ProviderComplianceScoreRange.Three, + ), + }); + } + + calculationResults.push({ + week: week, + results: weekResult, + }); + } + + return new SpsComplianceWeekResponseDto([ + await this.calculateSpsComplianceWeekDto( + calculationResults, + allocatorCount, + ProviderComplianceScoreRange.UpToOne, + ), + await this.calculateSpsComplianceWeekDto( + calculationResults, + allocatorCount, + ProviderComplianceScoreRange.Two, + ), + await this.calculateSpsComplianceWeekDto( + calculationResults, + allocatorCount, + ProviderComplianceScoreRange.Three, + ), + ]); + } + + private async calculateSpsComplianceWeekDto( + calculationResults: { + results: { + upToOneCompliantProvidersPercent: number; + twoCompliantProvidersPercent: number; + threeCompliantProvidersPercent: number; + }[]; + week: Date; + }[], + allocatorCount: number, + providerComplianceScoreRange: ProviderComplianceScoreRange, + ) { + return SpsComplianceWeekDto.of( + providerComplianceScoreRange, + await this.histogramHelper.getWeeklyHistogramResult( + this.getSpsComplianceBuckets( + calculationResults, + providerComplianceScoreRange, + ), + allocatorCount, + ), + ); + } + + private getSpsComplianceBuckets( + unsortedResults: { + results: { + upToOneCompliantProvidersPercent: number; + twoCompliantProvidersPercent: number; + threeCompliantProvidersPercent: number; + }[]; + week: Date; + }[], + providerComplianceScoreRange: ProviderComplianceScoreRange, + ): { + valueFromExclusive: number | null; + valueToInclusive: number | null; + count: number | null; + week: Date; + }[] { + let valueFromExclusive = -5; + const result: { + valueFromExclusive: number | null; + valueToInclusive: number | null; + count: number | null; + week: Date; + }[] = []; + do { + result.push( + ...unsortedResults.map((r) => { + const count = r.results.filter( + (p) => + this.getPercentValue(p, providerComplianceScoreRange) > + valueFromExclusive && + this.getPercentValue(p, providerComplianceScoreRange) <= + valueFromExclusive + 5, + ).length; + + return { + valueFromExclusive: valueFromExclusive, + valueToInclusive: valueFromExclusive + 5, + week: r.week, + count: count, + }; + }), + ); + + valueFromExclusive += 5; + } while (valueFromExclusive < 95); + + return result; + } + + private getPercentValue( + result: { + upToOneCompliantProvidersPercent: number; + twoCompliantProvidersPercent: number; + threeCompliantProvidersPercent: number; + }, + providerComplianceScoreRange: ProviderComplianceScoreRange, + ) { + switch (providerComplianceScoreRange) { + case ProviderComplianceScoreRange.UpToOne: + return result.upToOneCompliantProvidersPercent; + case ProviderComplianceScoreRange.Two: + return result.twoCompliantProvidersPercent; + case ProviderComplianceScoreRange.Three: + return result.threeCompliantProvidersPercent; + } + } + + private getAllocatorCompliantProvidersPercentage( + weekProvidersCompliance: { + complianceScore: number; + provider: string; + }[], + providers: string[], + complianceScore: ProviderComplianceScoreRange, + ) { + const validComplianceScores: number[] = []; + switch (complianceScore) { + case ProviderComplianceScoreRange.UpToOne: + validComplianceScores.push(0, 1); + break; + case ProviderComplianceScoreRange.Two: + validComplianceScores.push(2); + break; + case ProviderComplianceScoreRange.Three: + validComplianceScores.push(3); + break; + } + + return ( + (100 * + weekProvidersCompliance.filter( + (p) => + providers.includes(p.provider) && + validComplianceScores.includes(p.complianceScore), + ).length) / + weekProvidersCompliance.length + ); + } +} diff --git a/src/service/provider/provider.service.spec.ts b/src/service/provider/provider.service.spec.ts new file mode 100644 index 0000000..36de53b --- /dev/null +++ b/src/service/provider/provider.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProviderService } from './provider.service'; + +describe('ProviderService', () => { + let service: ProviderService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ProviderService], + }).compile(); + + service = module.get(ProviderService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/service/provider/provider.service.ts b/src/service/provider/provider.service.ts new file mode 100644 index 0000000..25166ce --- /dev/null +++ b/src/service/provider/provider.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../../db/prisma.service'; +import { + getProviderBiggestClientDistribution, + getProviderClientsWeekly, + getProviderRetrievability, +} from '../../../prisma/generated/client/sql'; +import { RetrievabilityWeekResponseDto } from '../../types/retrievabilityWeekResponse.dto'; +import { HistogramHelper } from '../../helper/histogram.helper'; +import { DateTime } from 'luxon'; + +@Injectable() +export class ProviderService { + constructor( + private readonly prismaService: PrismaService, + private readonly histogramHelper: HistogramHelper, + ) {} + + async getProviderClients() { + const providerCountResult = await this.prismaService.$queryRaw< + [ + { + count: number; + }, + ] + >`select count(distinct provider)::int + from client_provider_distribution_weekly`; + + return await this.histogramHelper.getWeeklyHistogramResult( + await this.prismaService.$queryRawTyped(getProviderClientsWeekly()), + providerCountResult[0].count, + ); + } + + async getProviderBiggestClientDistribution() { + const providerCountResult = await this.prismaService.$queryRaw< + [ + { + count: number; + }, + ] + >`select count(distinct provider)::int + from client_provider_distribution_weekly`; + + return await this.histogramHelper.getWeeklyHistogramResult( + await this.prismaService.$queryRawTyped( + getProviderBiggestClientDistribution(), + ), + providerCountResult[0].count, + ); + } + + async getProviderRetrievability() { + const providerCountAndAverageSuccessRate = await this.prismaService + .$queryRaw< + [ + { + count: number; + averageSuccessRate: number; + }, + ] + >`select count(distinct provider)::int, + 100 * avg(avg_retrievability_success_rate) as "averageSuccessRate" + from providers_weekly where week = ${DateTime.now().toUTC().minus({ week: 1 }).startOf('week').toJSDate()};`; + + const weeklyHistogramResult = + await this.histogramHelper.getWeeklyHistogramResult( + await this.prismaService.$queryRawTyped(getProviderRetrievability()), + providerCountAndAverageSuccessRate[0].count, + ); + + return RetrievabilityWeekResponseDto.of( + providerCountAndAverageSuccessRate[0].averageSuccessRate, + weeklyHistogramResult, + ); + } +} diff --git a/src/types/histogram.dto.ts b/src/types/histogram.dto.ts new file mode 100644 index 0000000..7d005b7 --- /dev/null +++ b/src/types/histogram.dto.ts @@ -0,0 +1,15 @@ +export class HistogramDto { + valueFromExclusive: number | null; + valueToInclusive: number | null; + count: number | null; + + constructor( + valueFromExclusive: number | null, + valueToInclusive: number | null, + count: number | null, + ) { + this.valueFromExclusive = valueFromExclusive; + this.valueToInclusive = valueToInclusive; + this.count = count; + } +} diff --git a/src/types/histogramWeek.dto.ts b/src/types/histogramWeek.dto.ts new file mode 100644 index 0000000..19add2d --- /dev/null +++ b/src/types/histogramWeek.dto.ts @@ -0,0 +1,13 @@ +import { HistogramDto } from './histogram.dto'; + +export class HistogramWeekDto { + week: Date; + results: HistogramDto[]; + total: number; + + constructor(week: Date, results: HistogramDto[], total: number) { + this.week = week; + this.results = results; + this.total = total; + } +} diff --git a/src/types/histogramWeek.response.dto.ts b/src/types/histogramWeek.response.dto.ts new file mode 100644 index 0000000..fec6367 --- /dev/null +++ b/src/types/histogramWeek.response.dto.ts @@ -0,0 +1,11 @@ +import { HistogramWeekDto } from './histogramWeek.dto'; + +export class HistogramWeekResponseDto { + total: number; + results: HistogramWeekDto[]; + + constructor(total: number, results: HistogramWeekDto[]) { + this.total = total; + this.results = results; + } +} diff --git a/src/types/providerComplianceScoreRange.ts b/src/types/providerComplianceScoreRange.ts new file mode 100644 index 0000000..b08ec6c --- /dev/null +++ b/src/types/providerComplianceScoreRange.ts @@ -0,0 +1,5 @@ +export enum ProviderComplianceScoreRange { + UpToOne, //0-1 + Two, //2-2 + Three, //3-3 +} diff --git a/src/types/retrievabilityWeekResponse.dto.ts b/src/types/retrievabilityWeekResponse.dto.ts new file mode 100644 index 0000000..f6efabb --- /dev/null +++ b/src/types/retrievabilityWeekResponse.dto.ts @@ -0,0 +1,18 @@ +import { HistogramWeekResponseDto } from './histogramWeek.response.dto'; + +export class RetrievabilityWeekResponseDto { + averageSuccessRate: number; + histogram: HistogramWeekResponseDto; + + public static of( + averageSuccessRate: number, + histogram: HistogramWeekResponseDto, + ): RetrievabilityWeekResponseDto { + const dto = new RetrievabilityWeekResponseDto(); + + dto.averageSuccessRate = averageSuccessRate; + dto.histogram = histogram; + + return dto; + } +} diff --git a/src/types/spsComplianceWeek.dto.ts b/src/types/spsComplianceWeek.dto.ts new file mode 100644 index 0000000..e53358d --- /dev/null +++ b/src/types/spsComplianceWeek.dto.ts @@ -0,0 +1,19 @@ +import { ProviderComplianceScoreRange } from './providerComplianceScoreRange'; +import { HistogramWeekResponseDto } from './histogramWeek.response.dto'; + +export class SpsComplianceWeekDto { + scoreRange: ProviderComplianceScoreRange; + histogram: HistogramWeekResponseDto; + + public static of( + scoreRange: ProviderComplianceScoreRange, + histogram: HistogramWeekResponseDto, + ) { + const dto = new SpsComplianceWeekDto(); + + dto.scoreRange = scoreRange; + dto.histogram = histogram; + + return dto; + } +} diff --git a/src/types/spsComplianceWeekResponse.dto.ts b/src/types/spsComplianceWeekResponse.dto.ts new file mode 100644 index 0000000..ab92546 --- /dev/null +++ b/src/types/spsComplianceWeekResponse.dto.ts @@ -0,0 +1,9 @@ +import { SpsComplianceWeekDto } from './spsComplianceWeek.dto'; + +export class SpsComplianceWeekResponseDto { + results: SpsComplianceWeekDto[]; + + constructor(results: SpsComplianceWeekDto[]) { + this.results = results; + } +}