diff --git a/eslint.config.mjs b/eslint.config.mjs index caebf6e..02d56dd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,9 +17,9 @@ export default tseslint.config( ...globals.node, ...globals.jest, }, - sourceType: 'commonjs', + sourceType: 'module', // Changé de 'commonjs' à 'module' parserOptions: { - projectService: true, + project: './tsconfig.json', // Ajout du chemin vers tsconfig tsconfigRootDir: import.meta.dirname, }, }, @@ -28,7 +28,13 @@ export default tseslint.config( rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn' + '@typescript-eslint/no-unsafe-argument': 'warn', + 'prettier/prettier': ['error', { + singleQuote: true, + trailingComma: 'all', + printWidth: 100, + tabWidth: 2, + }], }, }, ); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 66d762a..41abbc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,18 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/bull": "^11.0.4", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.0.1", + "@nestjs/swagger": "^11.2.1", "@prisma/client": "^6.17.1", + "cache-manager-redis-store": "^3.0.1", + "class-validator": "^0.14.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -713,6 +721,16 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@cacheable/utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.1.0.tgz", + "integrity": "sha512-ZdxfOiaarMqMj+H7qwlt5EBKWaeGihSYVHdQv5lUsbn8MJJOTW82OIwirQ39U5tMZkNvy3bQE+ryzC+xTAb9/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "keyv": "^5.5.3" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1341,6 +1359,13 @@ } } }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT", + "peer": true + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2045,6 +2070,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT", + "peer": true + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -2054,6 +2086,96 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2067,6 +2189,47 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@nestjs/bull": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-11.0.4.tgz", + "integrity": "sha512-QVz2PR/rJF/isy7otVnMTSqLf/O71p9Ka7lBZt9Gm+NQFv8fcH2L11GL7TA0whyCcw/kAX5iRepUXz/wed4JoA==", + "license": "MIT", + "dependencies": { + "@nestjs/bull-shared": "^11.0.4", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "bull": "^3.3 || ^4.0.0" + } + }, + "node_modules/@nestjs/bull-shared": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", + "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/cache-manager": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.1.tgz", + "integrity": "sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", + "cache-manager": ">=6", + "keyv": ">=5", + "rxjs": "^7.8.1" + } + }, "node_modules/@nestjs/cli": { "version": "11.0.10", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.10.tgz", @@ -2329,6 +2492,33 @@ } } }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@nestjs/core": { "version": "11.1.7", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz", @@ -2370,6 +2560,39 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.1.tgz", + "integrity": "sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==", + "license": "MIT", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz", @@ -2391,6 +2614,19 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.1.tgz", + "integrity": "sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==", + "license": "MIT", + "dependencies": { + "cron": "4.3.3" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.9", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", @@ -2489,6 +2725,39 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/swagger": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.1.tgz", + "integrity": "sha512-1MS7xf0pzc1mofG53xrrtrurnziafPUHkqzRm4YUVPA/egeiMaSerQBD/feiAeQ2BnX0WiLsTX4HQFO0icvOjQ==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.29.4" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.7", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.7.tgz", @@ -2703,6 +2972,78 @@ "@prisma/debug": "6.17.1" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -2979,6 +3320,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3081,6 +3428,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -4047,7 +4400,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -4364,6 +4716,25 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bull": { + "version": "4.16.5", + "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz", + "integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "cron-parser": "^4.9.0", + "get-port": "^5.1.1", + "ioredis": "^5.3.2", + "lodash": "^4.17.21", + "msgpackr": "^1.11.2", + "semver": "^7.5.2", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4413,6 +4784,29 @@ } } }, + "node_modules/cache-manager": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.4.tgz", + "integrity": "sha512-skmhkqXjPCBmrb70ctEx4zwFk7vb0RdFXlVGYWnFZ8pKvkzdFrFFKSJ1IaKduGfkryHOJvb7q2PkGmonmL+UGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@cacheable/utils": "^2.1.0", + "keyv": "^5.5.3" + } + }, + "node_modules/cache-manager-redis-store": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz", + "integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==", + "license": "MIT", + "dependencies": { + "redis": "^4.3.1" + }, + "engines": { + "node": ">= 16.18.0" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4576,6 +4970,17 @@ "dev": true, "license": "MIT" }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -4694,6 +5099,15 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4925,6 +5339,32 @@ "dev": true, "license": "MIT" }, + "node_modules/cron": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz", + "integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5029,6 +5469,16 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5045,6 +5495,17 @@ "devOptional": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5080,7 +5541,6 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5089,6 +5549,21 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5503,6 +5978,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -5860,6 +6341,16 @@ "node": ">=16" } }, + "node_modules/flat-cache/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", @@ -6041,6 +6532,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6095,6 +6595,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6473,6 +6986,31 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7483,7 +8021,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7567,13 +8104,13 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "license": "MIT", + "peer": true, "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/leven": { @@ -7600,6 +8137,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.24", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.24.tgz", + "integrity": "sha512-l5IlyL9AONj4voSd7q9xkuQOL4u8Ty44puTic7J88CmdXkxfGsRfoVLXHCxppwehgpb/Chdb80FFehHqjN3ItQ==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7660,9 +8203,22 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT", + "peer": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7704,6 +8260,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -7938,6 +8503,39 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "peer": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/multer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", @@ -8072,6 +8670,22 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8810,6 +9424,46 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "peer": true, + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -8999,7 +9653,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9240,6 +9893,13 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT", + "peer": true + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -9517,6 +10177,15 @@ "node": ">=8" } }, + "node_modules/swagger-ui-dist": { + "version": "5.29.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.4.tgz", + "integrity": "sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -10256,6 +10925,16 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -10278,6 +10957,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 5fdbc2e..9c88ad8 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,18 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/bull": "^11.0.4", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.0.1", + "@nestjs/swagger": "^11.2.1", "@prisma/client": "^6.17.1", + "cache-manager-redis-store": "^3.0.1", + "class-validator": "^0.14.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/prisma/migrations/20251021230409_init/migration.sql b/prisma/migrations/20251021230409_init/migration.sql new file mode 100644 index 0000000..ea239c3 --- /dev/null +++ b/prisma/migrations/20251021230409_init/migration.sql @@ -0,0 +1,38 @@ +/* + Warnings: + + - Added the required column `country` to the `Partner` table without a default value. This is not possible if the table is not empty. + - Added the required column `passwordHash` to the `Partner` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Partner" ADD COLUMN "companyInfo" JSONB, +ADD COLUMN "country" TEXT NOT NULL, +ADD COLUMN "keysRotatedAt" TIMESTAMP(3), +ADD COLUMN "passwordHash" TEXT NOT NULL, +ALTER COLUMN "status" SET DEFAULT 'PENDING'; + +-- CreateTable +CREATE TABLE "AuthSession" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "partnerId" TEXT NOT NULL, + "userId" TEXT, + "msisdn" TEXT NOT NULL, + "operator" TEXT NOT NULL, + "country" TEXT NOT NULL, + "authMethod" TEXT NOT NULL, + "challengeId" TEXT, + "status" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "AuthSession_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AuthSession_sessionId_key" ON "AuthSession"("sessionId"); + +-- AddForeignKey +ALTER TABLE "AuthSession" ADD CONSTRAINT "AuthSession_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4c4d2c6..bb3f5c7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -39,22 +39,7 @@ enum SubscriptionStatus { FAILED } -model Partner { - id String @id @default(cuid()) - name String - email String @unique - apiKey String @unique - secretKey String - status String - callbacks Json? - metadata Json? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - users User[] - subscriptions Subscription[] - payments Payment[] -} + model Operator { id String @id @default(cuid()) @@ -173,4 +158,44 @@ model Webhook { lastAttempt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt +} + +model Partner { + id String @id @default(cuid()) + name String + email String @unique + passwordHash String + apiKey String @unique + secretKey String + status String @default("PENDING") + companyInfo Json? + callbacks Json? + country String + metadata Json? + keysRotatedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users User[] + subscriptions Subscription[] + payments Payment[] + authSessions AuthSession[] +} + +model AuthSession { + id String @id @default(cuid()) + sessionId String @unique + partnerId String + userId String? + msisdn String + operator String + country String + authMethod String + challengeId String? + status String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + partner Partner @relation(fields: [partnerId], references: [id]) } \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 2f563c7..d2307e6 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -50,4 +50,4 @@ import { NotificationsModule } from './modules/notifications/notifications.modul providers: [PrismaService], exports: [PrismaService], }) -export class AppModule {} \ No newline at end of file +export class AppModule {} diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 55f42e2..40ac6d7 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -6,4 +6,4 @@ export default registerAs('app', () => ({ apiPrefix: process.env.API_PREFIX || 'v2', jwtSecret: process.env.JWT_SECRET || 'your-secret-key', jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d', -})); \ No newline at end of file +})); diff --git a/src/config/operators.config.ts b/src/config/operators.config.ts index bd57ec3..dffa6d0 100644 --- a/src/config/operators.config.ts +++ b/src/config/operators.config.ts @@ -30,7 +30,7 @@ export interface OperatorConfig { export const operatorsConfig = (): Record => ({ ORANGE_CIV: { - name: 'Orange Côte d\'Ivoire', + name: 'Orange Côte d Ivoire', baseUrl: process.env.ORANGE_CIV_BASE_URL || 'https://api.bizao.com', authType: 'OTP', endpoints: { @@ -87,4 +87,4 @@ export const operatorsConfig = (): Record => ({ response: 'MTNResponseTransformer', }, }, -}); \ No newline at end of file +}); diff --git a/src/main.ts b/src/main.ts index 246e66b..cd22145 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); // Global prefix - app.setGlobalPrefix('api/v2'); + app.setGlobalPrefix('api/v1'); // Validation app.useGlobalPipes( @@ -43,4 +43,4 @@ async function bootstrap() { console.log(`Application is running on: http://localhost:${port}`); console.log(`Swagger docs: http://localhost:${port}/api/docs`); } -bootstrap(); \ No newline at end of file +bootstrap(); diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..b7b2501 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { ApiKeyStrategy } from './strategies/api-key.strategy'; +import { PrismaService } from '../../shared/services/prisma.service'; +import { OperatorsModule } from '../operators/operators.module'; + +@Module({ + imports: [ + PassportModule.register({ defaultStrategy: 'jwt' }), + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('app.jwtSecret'), + signOptions: { + expiresIn: configService.get('app.jwtExpiresIn'), + }, + }), + inject: [ConfigService], + }), + OperatorsModule, + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy, ApiKeyStrategy, PrismaService], + exports: [AuthService, JwtModule], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..bb34f99 --- /dev/null +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,245 @@ +import { + Injectable, + UnauthorizedException, + BadRequestException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { PrismaService } from '../../shared/services/prisma.service'; +import { OperatorsService } from '../operators/operators.service'; +import * as bcrypt from 'bcrypt'; +import { AuthInitDto, AuthValidateDto, LoginDto } from './dto/auth.dto'; + +@Injectable() +export class AuthService { + constructor( + private readonly prisma: PrismaService, + private readonly jwtService: JwtService, + private readonly operatorsService: OperatorsService, + ) {} + + async initializeUserAuth(partnerId: string, dto: AuthInitDto) { + // Vérifier le partenaire + const partner = await this.prisma.partner.findUnique({ + where: { id: partnerId }, + }); + + if (!partner || partner.status !== 'ACTIVE') { + throw new UnauthorizedException('Invalid partner'); + } + + // Déterminer l'opérateur basé sur le numéro + const operator = this.detectOperator(dto.msisdn, dto.country); + + // Obtenir l'adaptateur approprié + const adapter = this.operatorsService.getAdapter(operator, dto.country); + + // Initialiser l'authentification avec l'opérateur + const authResponse = await adapter.initializeAuth({ + msisdn: dto.msisdn, + country: dto.country, + metadata: dto.metadata, + }); + + // Créer une session temporaire + const session = await this.prisma.authSession.create({ + data: { + sessionId: authResponse.sessionId, + partnerId: partnerId, + msisdn: dto.msisdn, + operator: operator, + country: dto.country, + authMethod: dto.authMethod, + challengeId: authResponse.challengeId, + status: 'PENDING', + expiresAt: authResponse.expiresAt, + }, + }); + + return { + sessionId: session.sessionId, + authMethod: dto.authMethod, + status: 'PENDING', + redirectUrl: authResponse.redirectUrl, + challengeId: authResponse.challengeId, + expiresAt: authResponse.expiresAt, + }; + } + + async validateUserAuth(dto: AuthValidateDto) { + // Récupérer la session + const session = await this.prisma.authSession.findUnique({ + where: { sessionId: dto.sessionId }, + }); + + if (!session) { + throw new BadRequestException('Invalid session'); + } + + if (session.status !== 'PENDING') { + throw new BadRequestException('Session already processed'); + } + + if (new Date() > session.expiresAt) { + throw new BadRequestException('Session expired'); + } + + // Obtenir l'adaptateur + const adapter = this.operatorsService.getAdapter( + session.operator, + session.country, + ); + + // Valider avec l'opérateur + const validationResponse = await adapter.validateAuth({ + challengeId: session.challengeId, + otpCode: dto.otpCode, + msisdn: session.msisdn, + country: session.country, + }); + + if (!validationResponse.success) { + await this.prisma.authSession.update({ + where: { id: session.id }, + data: { status: 'FAILED' }, + }); + throw new UnauthorizedException('Authentication failed'); + } + + // Créer ou mettre à jour l'utilisateur + const user = await this.prisma.user.upsert({ + where: { msisdn: session.msisdn }, + update: { + userToken: validationResponse.userToken, + userAlias: validationResponse.userAlias, + updatedAt: new Date(), + }, + create: { + msisdn: session.msisdn, + userToken: validationResponse.userToken, + userAlias: validationResponse.userAlias, + operatorId: await this.getOperatorId(session.operator, session.country), + partnerId: session.partnerId, + country: session.country, + }, + }); + + // Mettre à jour la session + await this.prisma.authSession.update({ + where: { id: session.id }, + data: { + status: 'SUCCESS', + userId: user.id, + }, + }); + + // Créer un JWT pour le partenaire + const payload = { + userId: user.id, + partnerId: session.partnerId, + msisdn: user.msisdn, + operator: session.operator, + }; + + return { + success: true, + accessToken: this.jwtService.sign(payload), + userToken: validationResponse.userToken, + userAlias: validationResponse.userAlias, + msisdn: session.msisdn, + operator: session.operator, + country: session.country, + expiresAt: validationResponse.expiresAt, + }; + } + + async loginPartner(dto: LoginDto) { + const partner = await this.prisma.partner.findUnique({ + where: { email: dto.email }, + }); + + if (!partner) { + throw new UnauthorizedException('Invalid credentials'); + } + + const isPasswordValid = await bcrypt.compare( + dto.password, + partner.passwordHash, + ); + + if (!isPasswordValid) { + throw new UnauthorizedException('Invalid credentials'); + } + + const payload = { + partnerId: partner.id, + email: partner.email, + type: 'partner', + }; + + return { + accessToken: this.jwtService.sign(payload), + partner: { + id: partner.id, + name: partner.name, + email: partner.email, + status: partner.status, + }, + }; + } + + private detectOperator(msisdn: string, country: string): string { + // Logique pour détecter l'opérateur basé sur le préfixe + const prefixMap = { + CI: { + '07': 'ORANGE', + '08': 'ORANGE', + '09': 'ORANGE', + '04': 'MTN', + '05': 'MTN', + '06': 'MTN', + '01': 'MOOV', + }, + SN: { + '77': 'ORANGE', + '78': 'ORANGE', + '76': 'FREE', + '70': 'EXPRESSO', + }, + // Ajouter d'autres pays + }; + + const countryPrefixes = prefixMap[country]; + if (!countryPrefixes) { + throw new BadRequestException(`Country ${country} not supported`); + } + + const prefix = msisdn.substring(0, 2); + const operator = countryPrefixes[prefix]; + + if (!operator) { + throw new BadRequestException(`Cannot detect operator for ${msisdn}`); + } + + return operator; + } + + private async getOperatorId( + operatorCode: string, + country: string, + ): Promise { + const operator = await this.prisma.operator.findFirst({ + where: { + code: operatorCode as any, + country: country, + }, + }); + + if (!operator) { + throw new BadRequestException( + `Operator ${operatorCode} not found in ${country}`, + ); + } + + return operator.id; + } +} diff --git a/src/modules/auth/dto/auth.dto.ts b/src/modules/auth/dto/auth.dto.ts new file mode 100644 index 0000000..4eb39a5 --- /dev/null +++ b/src/modules/auth/dto/auth.dto.ts @@ -0,0 +1,50 @@ +import { IsString, IsEnum, IsOptional, IsMobilePhone } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class AuthInitDto { + @ApiProperty() + @IsMobilePhone() + msisdn: string; + + @ApiProperty() + @IsString() + country: string; + + @ApiProperty({ enum: ['OTP_SMS', 'REDIRECT_3G', 'SMS_MO', 'USSD'] }) + @IsEnum(['OTP_SMS', 'REDIRECT_3G', 'SMS_MO', 'USSD']) + authMethod: string; + + @ApiProperty({ required: false }) + @IsOptional() + redirectUrl?: string; + + @ApiProperty({ required: false }) + @IsOptional() + metadata?: Record; +} + +export class AuthValidateDto { + @ApiProperty() + @IsString() + sessionId: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + otpCode?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + challengeResponse?: string; +} + +export class LoginDto { + @ApiProperty() + @IsString() + email: string; + + @ApiProperty() + @IsString() + password: string; +} diff --git a/src/modules/auth/strategies/jwt.strategy.ts b/src/modules/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..718b472 --- /dev/null +++ b/src/modules/auth/strategies/jwt.strategy.ts @@ -0,0 +1,23 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('app.jwtSecret'), + }); + } + + async validate(payload: any) { + return { + userId: payload.userId, + partnerId: payload.partnerId, + email: payload.email, + }; + } +} diff --git a/src/modules/operators/adapters/operator-adapter.factory.ts b/src/modules/operators/adapters/operator-adapter.factory.ts index 02c26ee..fb36442 100644 --- a/src/modules/operators/adapters/operator-adapter.factory.ts +++ b/src/modules/operators/adapters/operator-adapter.factory.ts @@ -12,13 +12,13 @@ export class OperatorAdapterFactory { getAdapter(operator: string, country: string): IOperatorAdapter { const key = `${operator}_${country}`.toUpperCase(); - + const adapterMap = { - 'ORANGE_CI': this.orangeAdapter, - 'ORANGE_SN': this.orangeAdapter, - 'ORANGE_CM': this.orangeAdapter, - 'MTN_CI': this.mtnAdapter, - 'MTN_CM': this.mtnAdapter, + ORANGE_CI: this.orangeAdapter, + ORANGE_SN: this.orangeAdapter, + ORANGE_CM: this.orangeAdapter, + MTN_CI: this.mtnAdapter, + MTN_CM: this.mtnAdapter, // Ajouter d'autres mappings }; @@ -29,4 +29,4 @@ export class OperatorAdapterFactory { return adapter; } -} \ No newline at end of file +} diff --git a/src/modules/operators/adapters/operator.adapter.interface.ts b/src/modules/operators/adapters/operator.adapter.interface.ts index bb34d6c..c7269f9 100644 --- a/src/modules/operators/adapters/operator.adapter.interface.ts +++ b/src/modules/operators/adapters/operator.adapter.interface.ts @@ -4,7 +4,9 @@ export interface IOperatorAdapter { charge(params: ChargeParams): Promise; refund(params: RefundParams): Promise; sendSms(params: SmsParams): Promise; - createSubscription?(params: SubscriptionParams): Promise; + createSubscription?( + params: SubscriptionParams, + ): Promise; cancelSubscription?(subscriptionId: string): Promise; } @@ -37,4 +39,4 @@ export interface ChargeResponse { operatorReference: string; amount: number; currency: string; -} \ No newline at end of file +} diff --git a/src/modules/operators/adapters/orange.adapter.ts b/src/modules/operators/adapters/orange.adapter.ts index 922be91..af4580a 100644 --- a/src/modules/operators/adapters/orange.adapter.ts +++ b/src/modules/operators/adapters/orange.adapter.ts @@ -28,7 +28,7 @@ export class OrangeAdapter implements IOperatorAdapter { async initializeAuth(params: AuthInitParams): Promise { const countryCode = this.getCountryCode(params.country); - + const bizaoRequest = { challenge: { method: 'OTP-SMS-AUTH', @@ -123,8 +123,8 @@ export class OrangeAdapter implements IOperatorAdapter { ); const result = response.data.challenge.result; - const userToken = result.find(r => r.type === 'OrangeApiToken')?.value; - const userAlias = result.find(r => r.type === 'ise2')?.value; + const userToken = result.find((r) => r.type === 'OrangeApiToken')?.value; + const userAlias = result.find((r) => r.type === 'ise2')?.value; return { success: true, @@ -256,4 +256,4 @@ export class OrangeAdapter implements IOperatorAdapter { }; return senderMap[country]; } -} \ No newline at end of file +} diff --git a/src/modules/operators/adapters/orange.transformer.ts b/src/modules/operators/adapters/orange.transformer.ts index ea4f171..79e90d0 100644 --- a/src/modules/operators/adapters/orange.transformer.ts +++ b/src/modules/operators/adapters/orange.transformer.ts @@ -5,20 +5,26 @@ export class OrangeTransformer { transformChargeResponse(bizaoResponse: any): any { return { paymentId: bizaoResponse.amountTransaction?.serverReferenceCode, - status: this.mapStatus(bizaoResponse.amountTransaction?.transactionOperationStatus), + status: this.mapStatus( + bizaoResponse.amountTransaction?.transactionOperationStatus, + ), operatorReference: bizaoResponse.amountTransaction?.serverReferenceCode, - amount: parseFloat(bizaoResponse.amountTransaction?.paymentAmount?.totalAmountCharged), - currency: bizaoResponse.amountTransaction?.paymentAmount?.chargingInformation?.currency, + amount: parseFloat( + bizaoResponse.amountTransaction?.paymentAmount?.totalAmountCharged, + ), + currency: + bizaoResponse.amountTransaction?.paymentAmount?.chargingInformation + ?.currency, createdAt: new Date(), }; } private mapStatus(bizaoStatus: string): string { const statusMap = { - 'Charged': 'SUCCESS', - 'Failed': 'FAILED', - 'Pending': 'PENDING', + Charged: 'SUCCESS', + Failed: 'FAILED', + Pending: 'PENDING', }; return statusMap[bizaoStatus] || 'PENDING'; } -} \ No newline at end of file +} diff --git a/src/modules/operators/operators.module.ts b/src/modules/operators/operators.module.ts index aed0f5c..1731a48 100644 --- a/src/modules/operators/operators.module.ts +++ b/src/modules/operators/operators.module.ts @@ -28,4 +28,4 @@ import { PrismaService } from '../../shared/services/prisma.service'; ], exports: [OperatorsService], }) -export class OperatorsModule {} \ No newline at end of file +export class OperatorsModule {} diff --git a/src/modules/partners/dto/partner.dto.ts b/src/modules/partners/dto/partner.dto.ts new file mode 100644 index 0000000..0a76c84 --- /dev/null +++ b/src/modules/partners/dto/partner.dto.ts @@ -0,0 +1,76 @@ +import { + IsString, + IsEmail, + IsOptional, + IsObject, + MinLength, +} from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreatePartnerDto { + @ApiProperty() + @IsString() + name: string; + + @ApiProperty() + @IsEmail() + email: string; + + @ApiProperty() + @IsString() + @MinLength(8) + password: string; + + @ApiProperty() + @IsString() + country: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsObject() + companyInfo?: { + legalName: string; + taxId: string; + address: string; + phone?: string; + website?: string; + }; + + @ApiProperty({ required: false }) + @IsOptional() + metadata?: Record; +} + +export class UpdateCallbacksDto { + @ApiProperty({ required: false }) + @IsOptional() + headerEnrichment?: { + url: string; + method: string; + headers?: Record; + }; + + @ApiProperty({ required: false }) + @IsOptional() + subscription?: { + onCreate?: string; + onRenew?: string; + onCancel?: string; + onExpire?: string; + }; + + @ApiProperty({ required: false }) + @IsOptional() + payment?: { + onSuccess?: string; + onFailure?: string; + onRefund?: string; + }; + + @ApiProperty({ required: false }) + @IsOptional() + authentication?: { + onSuccess?: string; + onFailure?: string; + }; +} diff --git a/src/modules/partners/partners.module.ts b/src/modules/partners/partners.module.ts new file mode 100644 index 0000000..e2e1340 --- /dev/null +++ b/src/modules/partners/partners.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PartnersController } from './partners.controller'; +import { PartnersService } from './partners.service'; +import { PrismaService } from '../../shared/services/prisma.service'; + +@Module({ + controllers: [PartnersController], + providers: [PartnersService, PrismaService], + exports: [PartnersService], +}) +export class PartnersModule {} diff --git a/src/modules/partners/partners.service.ts b/src/modules/partners/partners.service.ts new file mode 100644 index 0000000..8ac12b2 --- /dev/null +++ b/src/modules/partners/partners.service.ts @@ -0,0 +1,184 @@ +import { + Injectable, + ConflictException, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from '../../shared/services/prisma.service'; +import * as bcrypt from 'bcrypt'; +import * as crypto from 'crypto'; +import { CreatePartnerDto, UpdateCallbacksDto } from './dto/partner.dto'; + +@Injectable() +export class PartnersService { + constructor(private readonly prisma: PrismaService) {} + + async register(dto: CreatePartnerDto) { + // Vérifier si l'email existe déjà + const existingPartner = await this.prisma.partner.findUnique({ + where: { email: dto.email }, + }); + + if (existingPartner) { + throw new ConflictException('Email already registered'); + } + + // Générer les clés API + const apiKey = this.generateApiKey(); + const secretKey = this.generateSecretKey(); + + // Hasher le mot de passe + const passwordHash = await bcrypt.hash(dto.password, 10); + + // Créer le partenaire + const partner = await this.prisma.partner.create({ + data: { + name: dto.name, + email: dto.email, + passwordHash: passwordHash, + apiKey: apiKey, + secretKey: secretKey, + status: 'PENDING', + companyInfo: dto.companyInfo, + country: dto.country, + metadata: dto.metadata, + }, + }); + + return { + partnerId: partner.id, + apiKey: partner.apiKey, + secretKey: partner.secretKey, + status: partner.status, + message: 'Partner registered successfully. Awaiting approval.', + }; + } + + async updateCallbacks(partnerId: string, dto: UpdateCallbacksDto) { + const partner = await this.prisma.partner.findUnique({ + where: { id: partnerId }, + }); + + if (!partner) { + throw new NotFoundException('Partner not found'); + } + + const updatedPartner = await this.prisma.partner.update({ + where: { id: partnerId }, + data: { + callbacks: dto, + }, + }); + + return { + partnerId: updatedPartner.id, + callbacks: updatedPartner.callbacks, + }; + } + + async getPartner(partnerId: string) { + const partner = await this.prisma.partner.findUnique({ + where: { id: partnerId }, + select: { + id: true, + name: true, + email: true, + status: true, + callbacks: true, + companyInfo: true, + createdAt: true, + _count: { + select: { + users: true, + subscriptions: true, + payments: true, + }, + }, + }, + }); + + if (!partner) { + throw new NotFoundException('Partner not found'); + } + + return partner; + } + + async getPartnerStats(partnerId: string) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const [totalUsers, activeSubscriptions, todayPayments, monthRevenue] = + await Promise.all([ + this.prisma.user.count({ + where: { partnerId }, + }), + this.prisma.subscription.count({ + where: { + partnerId, + status: 'ACTIVE', + }, + }), + this.prisma.payment.count({ + where: { + partnerId, + createdAt: { gte: today }, + }, + }), + this.prisma.payment.aggregate({ + where: { + partnerId, + status: 'SUCCESS', + createdAt: { + gte: new Date(today.getFullYear(), today.getMonth(), 1), + }, + }, + _sum: { + amount: true, + }, + }), + ]); + + return { + totalUsers, + activeSubscriptions, + todayPayments, + monthRevenue: monthRevenue._sum.amount || 0, + }; + } + + async regenerateKeys(partnerId: string) { + const partner = await this.prisma.partner.findUnique({ + where: { id: partnerId }, + }); + + if (!partner) { + throw new NotFoundException('Partner not found'); + } + + const newApiKey = this.generateApiKey(); + const newSecretKey = this.generateSecretKey(); + + const updatedPartner = await this.prisma.partner.update({ + where: { id: partnerId }, + data: { + apiKey: newApiKey, + secretKey: newSecretKey, + keysRotatedAt: new Date(), + }, + }); + + return { + apiKey: updatedPartner.apiKey, + secretKey: updatedPartner.secretKey, + message: 'Keys regenerated successfully', + }; + } + + private generateApiKey(): string { + return `pk_${crypto.randomBytes(32).toString('hex')}`; + } + + private generateSecretKey(): string { + return `sk_${crypto.randomBytes(32).toString('hex')}`; + } +} diff --git a/src/modules/payments/dto/charge.dto.ts b/src/modules/payments/dto/charge.dto.ts index 72ce5f4..3364545 100644 --- a/src/modules/payments/dto/charge.dto.ts +++ b/src/modules/payments/dto/charge.dto.ts @@ -32,4 +32,4 @@ export class ChargeDto { @ApiProperty({ required: false }) @IsOptional() metadata?: Record; -} \ No newline at end of file +} diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts new file mode 100644 index 0000000..f2c68b3 --- /dev/null +++ b/src/modules/payments/payments.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { BullModule } from '@nestjs/bull'; +import { PaymentsController } from './payments.controller'; +import { PaymentsService } from './payments.service'; +import { PaymentProcessor } from './processors/payment.processor'; +import { WebhookService } from './services/webhook.service'; +import { PrismaService } from '../../shared/services/prisma.service'; +import { OperatorsModule } from '../operators/operators.module'; + +@Module({ + imports: [ + BullModule.registerQueue({ + name: 'payments', + }), + BullModule.registerQueue({ + name: 'webhooks', + }), + OperatorsModule, + ], + controllers: [PaymentsController], + providers: [PaymentsService, PaymentProcessor, WebhookService, PrismaService], + exports: [PaymentsService], +}) +export class PaymentsModule {} diff --git a/src/modules/payments/payments.service.ts b/src/modules/payments/payments.service.ts index 1192fc5..d9478ca 100644 --- a/src/modules/payments/payments.service.ts +++ b/src/modules/payments/payments.service.ts @@ -59,9 +59,10 @@ export class PaymentsService { const updatedPayment = await this.prisma.payment.update({ where: { id: payment.id }, data: { - status: result.status === 'SUCCESS' - ? PaymentStatus.SUCCESS - : PaymentStatus.FAILED, + status: + result.status === 'SUCCESS' + ? PaymentStatus.SUCCESS + : PaymentStatus.FAILED, operatorReference: result.operatorReference, completedAt: new Date(), }, @@ -101,4 +102,4 @@ export class PaymentsService { // Implémenter la notification webhook // Utiliser Bull Queue pour gérer les retries } -} \ No newline at end of file +} diff --git a/src/modules/payments/processors/payment.processor.ts b/src/modules/payments/processors/payment.processor.ts new file mode 100644 index 0000000..397e055 --- /dev/null +++ b/src/modules/payments/processors/payment.processor.ts @@ -0,0 +1,49 @@ +import { Process, Processor } from '@nestjs/bull'; +import { Job } from 'bull'; +import { PaymentsService } from '../payments.service'; +import { WebhookService } from '../services/webhook.service'; + +@Processor('payments') +export class PaymentProcessor { + constructor( + private readonly paymentsService: PaymentsService, + private readonly webhookService: WebhookService, + ) {} + + @Process('process-payment') + async handlePayment(job: Job) { + const { paymentId } = job.data; + + try { + // Traiter le paiement + const result = await this.paymentsService.processPayment(paymentId); + + // Envoyer le webhook + if (result.callbackUrl) { + await this.webhookService.send({ + url: result.callbackUrl, + event: + result.status === 'SUCCESS' ? 'PAYMENT_SUCCESS' : 'PAYMENT_FAILED', + payload: result, + }); + } + + return result; + } catch (error) { + console.error(`Payment processing failed for ${paymentId}:`, error); + throw error; + } + } + + @Process('retry-payment') + async handleRetry(job: Job) { + const { paymentId, attempt } = job.data; + + try { + return await this.paymentsService.retryPayment(paymentId, attempt); + } catch (error) { + console.error(`Payment retry failed for ${paymentId}:`, error); + throw error; + } + } +} diff --git a/src/modules/subscriptions/subscriptions.service.ts b/src/modules/subscriptions/subscriptions.service.ts index 72c8cb3..67d09ae 100644 --- a/src/modules/subscriptions/subscriptions.service.ts +++ b/src/modules/subscriptions/subscriptions.service.ts @@ -61,7 +61,7 @@ export class SubscriptionsService { if (payment.status === 'SUCCESS') { await this.prisma.subscription.update({ where: { id: subscription.id }, - data: { + data: { status: SubscriptionStatus.ACTIVE, lastPaymentId: payment.id, }, @@ -78,7 +78,7 @@ export class SubscriptionsService { // Activer en période d'essai await this.prisma.subscription.update({ where: { id: subscription.id }, - data: { + data: { status: SubscriptionStatus.TRIAL, trialEndsAt: this.calculateTrialEnd(dto.trialPeriod), }, @@ -136,7 +136,10 @@ export class SubscriptionsService { await this.handlePaymentFailure(subscription.id); } } catch (error) { - console.error(`Failed to renew subscription ${subscription.id}:`, error); + console.error( + `Failed to renew subscription ${subscription.id}:`, + error, + ); await this.handlePaymentFailure(subscription.id); } } @@ -166,9 +169,13 @@ export class SubscriptionsService { const now = new Date(); switch (trialPeriod.unit) { case 'DAYS': - return new Date(now.getTime() + trialPeriod.duration * 24 * 60 * 60 * 1000); + return new Date( + now.getTime() + trialPeriod.duration * 24 * 60 * 60 * 1000, + ); case 'WEEKS': - return new Date(now.getTime() + trialPeriod.duration * 7 * 24 * 60 * 60 * 1000); + return new Date( + now.getTime() + trialPeriod.duration * 7 * 24 * 60 * 60 * 1000, + ); case 'MONTHS': return new Date(now.setMonth(now.getMonth() + trialPeriod.duration)); default: @@ -204,4 +211,4 @@ export class SubscriptionsService { }); } } -} \ No newline at end of file +} diff --git a/src/shared/services/prisma.service.ts b/src/shared/services/prisma.service.ts index fd0aa0b..5541f1e 100644 --- a/src/shared/services/prisma.service.ts +++ b/src/shared/services/prisma.service.ts @@ -2,7 +2,10 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ constructor() { super({ log: ['query', 'info', 'warn', 'error'], @@ -16,4 +19,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul async onModuleDestroy() { await this.$disconnect(); } -} \ No newline at end of file +}