diff --git a/services/email_service/.env.sample b/services/email_service/.env.sample index de94810..7bf9ca1 100644 --- a/services/email_service/.env.sample +++ b/services/email_service/.env.sample @@ -10,4 +10,3 @@ MAIL_HOST= MAIL_PORT= SMTP_USER= SMTP_PASS= -MAIL_FROM="Framez " diff --git a/services/email_service/README.md b/services/email_service/README.md index 576f97b..9bcf83c 100644 --- a/services/email_service/README.md +++ b/services/email_service/README.md @@ -90,7 +90,6 @@ MAIL_HOST= MAIL_PORT= SMTP_USER= SMTP_PASS= -MAIL_FROM="Framez " ``` #### 4. Run PostgreSQL and RabbitMQ (via Docker Compose or local): diff --git a/services/email_service/package.json b/services/email_service/package.json index b2ae385..8fa834b 100644 --- a/services/email_service/package.json +++ b/services/email_service/package.json @@ -17,18 +17,33 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "typeorm": "typeorm-ts-node-esm", + "migration:generate": "ts-node ./node_modules/typeorm/cli.js migration:generate -d src/config/data-source.ts", + "migration:run": "ts-node ./node_modules/typeorm/cli.js migration:run -d src/config/data-source.ts", + "migration:revert": "ts-node ./node_modules/typeorm/cli.js migration:revert -d src/config/data-source.ts" }, "dependencies": { "@nestjs-modules/mailer": "^2.0.2", + "@nestjs/axios": "^4.0.1", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/microservices": "^11.1.8", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.1", + "@nestjs/terminus": "^11.0.0", "@nestjs/typeorm": "^11.0.0", + "@sendgrid/mail": "^8.1.6", "amqp-connection-manager": "^5.0.0", "amqplib": "^0.10.9", + "cache-manager": "^7.2.4", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "dotenv": "^17.2.3", + "ioredis": "^5.8.2", + "mailersend": "^2.6.0", "nodemailer": "^7.0.10", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", diff --git a/services/email_service/pnpm-lock.yaml b/services/email_service/pnpm-lock.yaml index 5541d87..6d15328 100644 --- a/services/email_service/pnpm-lock.yaml +++ b/services/email_service/pnpm-lock.yaml @@ -10,31 +10,64 @@ importers: dependencies: '@nestjs-modules/mailer': specifier: ^2.0.2 - version: 2.0.2(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(nodemailer@7.0.10) + version: 2.0.2(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(nodemailer@7.0.10) + '@nestjs/axios': + specifier: ^4.0.1 + version: 4.0.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) + '@nestjs/cache-manager': + specifier: ^3.0.1 + version: 3.0.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(cache-manager@7.2.4)(keyv@5.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: ^11.0.1 - version: 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.1 - version: 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/microservices': specifier: ^11.1.8 - version: 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(cache-manager@7.2.4)(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.1 - version: 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8) + version: 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8) + '@nestjs/swagger': + specifier: ^11.2.1 + version: 11.2.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + '@nestjs/terminus': + specifier: ^11.0.0 + version: 11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@nestjs/microservices@11.1.8)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3))) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3))) + version: 11.0.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3))) + '@sendgrid/mail': + specifier: ^8.1.6 + version: 8.1.6 amqp-connection-manager: specifier: ^5.0.0 version: 5.0.0(amqplib@0.10.9) amqplib: specifier: ^0.10.9 version: 0.10.9 + cache-manager: + specifier: ^7.2.4 + version: 7.2.4 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.2 + version: 0.14.2 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + ioredis: + specifier: ^5.8.2 + version: 5.8.2 + mailersend: + specifier: ^2.6.0 + version: 2.6.0 nodemailer: specifier: ^7.0.10 version: 7.0.10 @@ -49,7 +82,7 @@ importers: version: 7.8.2 typeorm: specifier: ^0.3.27 - version: 0.3.27(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) + version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -65,7 +98,7 @@ importers: version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.1 - version: 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8) + version: 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8) '@types/amqplib': specifier: ^0.10.8 version: 0.10.8 @@ -457,6 +490,9 @@ packages: '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@cacheable/utils@2.2.0': + resolution: {integrity: sha512-7xaQayO3msdVcxXLYcLU5wDqJBNdQcPPPHr6mdTEIQI7N7TbtSVVTpWOTfjyhg0L6AQwQdq7miKdWtTDBoBldQ==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -735,6 +771,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.4.0': + resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -859,10 +898,16 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -873,6 +918,22 @@ packages: '@nestjs/core': '>=7.0.9' nodemailer: '>=6.4.6' + '@nestjs/axios@4.0.1': + resolution: {integrity: sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + axios: ^1.3.1 + rxjs: ^7.0.0 + + '@nestjs/cache-manager@3.0.1': + resolution: {integrity: sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==} + 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 + '@nestjs/cli@11.0.10': resolution: {integrity: sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==} engines: {node: '>= 20.11'} @@ -923,6 +984,19 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/mapped-types@2.1.0': + resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} + 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 + '@nestjs/microservices@11.1.8': resolution: {integrity: sha512-AybVmZHzJtSnf3igmwH79FpwFjPZejtWTcWP29bT7QoPmDPg5A7/hPuWA6/Wf2cD6U37EAaX83+R4jCnR5E+6A==} peerDependencies: @@ -970,6 +1044,71 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/swagger@11.2.1': + resolution: {integrity: sha512-1MS7xf0pzc1mofG53xrrtrurnziafPUHkqzRm4YUVPA/egeiMaSerQBD/feiAeQ2BnX0WiLsTX4HQFO0icvOjQ==} + 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 + + '@nestjs/terminus@11.0.0': + resolution: {integrity: sha512-c55LOo9YGovmQHtFUMa/vDaxGZ2cglMTZejqgHREaApt/GArTfgYYGwhRXPLq8ZwiQQlLuYB+79e9iA8mlDSLA==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + '@nestjs/microservices': ^10.0.0 || ^11.0.0 + '@nestjs/mongoose': ^11.0.0 + '@nestjs/sequelize': ^10.0.0 || ^11.0.0 + '@nestjs/typeorm': ^10.0.0 || ^11.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + '@nestjs/testing@11.1.8': resolution: {integrity: sha512-E6K+0UTKztcPxJzLnQa7S34lFjZbrj3Z1r7c5y5WDrL1m5HD1H4AeyBhicHgdaFmxjLAva2bq0sYKy/S7cdeYA==} peerDependencies: @@ -1027,9 +1166,24 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sendgrid/client@8.1.6': + resolution: {integrity: sha512-/BHu0hqwXNHr2aLhcXU7RmmlVqrdfrbY9KpaNj00KZHlVOVoRxRVrpOCabIB+91ISXJ6+mLM9vpaVUhK6TwBWA==} + engines: {node: '>=12.*'} + + '@sendgrid/helpers@8.0.0': + resolution: {integrity: sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==} + engines: {node: '>= 12.0.0'} + + '@sendgrid/mail@8.1.6': + resolution: {integrity: sha512-/ZqxUvKeEztU9drOoPC/8opEPOk+jLlB2q4+xpx6HVLq6aFu3pMpalkTpAQz8XfRfpLp8O25bh6pGPcHDCYpqg==} + engines: {node: '>=12.*'} + '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} @@ -1341,6 +1495,9 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/validator@13.15.8': + resolution: {integrity: sha512-/NAHBJ0RwpsbLzzbLoLm/GnvCGB+A0/p5S61RUIsh7j3MP2dMkdUbWNdFqnluLlUheAs1CR2GlX2R7uzb7Tc0w==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1591,6 +1748,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1637,6 +1798,9 @@ packages: resolution: {integrity: sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==} engines: {node: '>=10'} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1716,6 +1880,9 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + babel-jest@30.2.0: resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1772,6 +1939,10 @@ packages: bowser@2.12.1: resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1814,6 +1985,9 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cache-manager@7.2.4: + resolution: {integrity: sha512-skmhkqXjPCBmrb70ctEx4zwFk7vb0RdFXlVGYWnFZ8pKvkzdFrFFKSJ1IaKduGfkryHOJvb7q2PkGmonmL+UGw==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1862,6 +2036,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -1892,10 +2070,20 @@ packages: cjs-module-lexer@2.1.1: resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.2: + resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==} + clean-css@4.2.4: resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} engines: {node: '>= 4.0'} + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1920,6 +2108,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -2033,6 +2225,10 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} @@ -2147,6 +2343,10 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2363,6 +2563,9 @@ packages: extend-object@1.0.0: resolution: {integrity: sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2395,6 +2598,10 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -2436,6 +2643,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2455,6 +2671,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + formidable@3.5.4: resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} engines: {node: '>=14.0.0'} @@ -2485,6 +2705,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -2620,6 +2844,10 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2666,6 +2894,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ioredis@5.8.2: + resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} + engines: {node: '>=12.22.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2756,6 +2988,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-unfetch@4.0.2: + resolution: {integrity: sha512-1Yd+CF/7al18/N2BDbsLBcp6RO3tucSW+jcLq24dqdX5MNbCNTw1z4BsGsp4zNmjr/Izm2cs/cEqZPp4kvWSCA==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -2993,6 +3228,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.5.4: + resolution: {integrity: sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -3010,6 +3248,9 @@ packages: libmime@5.3.7: resolution: {integrity: sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==} + libphonenumber-js@1.12.26: + resolution: {integrity: sha512-MagMOuqEXB2Pa90cWE+BoCmcKJx+de5uBIicaUkQ+uiEslZ0OBMNOkSZT/36syXNHu68UeayTxPm3DYM2IHoLQ==} + libqp@2.1.1: resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==} @@ -3040,6 +3281,12 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -3080,6 +3327,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + mailersend@2.6.0: + resolution: {integrity: sha512-YX2Gyc6Wyw4Q4IsJ4np2Reof8nFWQ2OP/yXZTZcGCz4B7B1BOAYs71Kjb1uNRTTfDChP7rzzWBoOaX6iNlvPAg==} + mailparser@3.9.0: resolution: {integrity: sha512-jpaNLhDjwy0w2f8sySOSRiWREjPqssSc0C2czV98btCXCRX3EyNloQ2IWirmMDj1Ies8Fkm0l96bZBZpDG7qkg==} @@ -3328,6 +3578,11 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} @@ -3340,6 +3595,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -3620,6 +3879,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pug-attrs@3.0.0: resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} @@ -3707,6 +3969,14 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3906,6 +4176,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -3992,6 +4265,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-ui-dist@5.29.4: + resolution: {integrity: sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -4135,6 +4411,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -4250,6 +4530,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unfetch@5.0.0: + resolution: {integrity: sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -4298,6 +4581,10 @@ packages: resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} engines: {node: '>=10'} + validator@13.15.23: + resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -4320,6 +4607,10 @@ packages: resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} engines: {node: '>=10.0.0'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -4357,6 +4648,10 @@ packages: engines: {node: '>= 8'} hasBin: true + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + with@7.0.2: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} @@ -5052,6 +5347,10 @@ snapshots: '@borewit/text-codec@0.1.1': {} + '@cacheable/utils@2.2.0': + dependencies: + keyv: 5.5.4 + '@colors/colors@1.5.0': optional: true @@ -5315,6 +5614,8 @@ snapshots: optionalDependencies: '@types/node': 22.19.0 + '@ioredis/commands@1.4.0': {} + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -5548,8 +5849,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@keyv/serialize@1.1.1': {} + '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.15.1': {} + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.7.0 @@ -5557,11 +5862,11 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(nodemailer@7.0.10)': + '@nestjs-modules/mailer@2.0.2(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(nodemailer@7.0.10)': dependencies: '@css-inline/css-inline': 0.14.1 - '@nestjs/common': 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) glob: 10.3.12 nodemailer: 7.0.10 optionalDependencies: @@ -5577,6 +5882,20 @@ snapshots: transitivePeerDependencies: - encoding + '@nestjs/axios@4.0.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.13.2 + rxjs: 7.8.2 + + '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(cache-manager@7.2.4)(keyv@5.5.4)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cache-manager: 7.2.4 + keyv: 5.5.4 + rxjs: 7.8.2 + '@nestjs/cli@11.0.10(@types/node@22.19.0)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) @@ -5604,7 +5923,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.0.0 iterare: 1.2.1 @@ -5613,20 +5932,23 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.2(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.2(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) dotenv: 16.4.7 dotenv-expand: 12.0.1 lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -5636,13 +5958,21 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/microservices': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8) + '@nestjs/microservices': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(cache-manager@7.2.4)(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8) + + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 - '@nestjs/microservices@11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/microservices@11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(cache-manager@7.2.4)(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 reflect-metadata: 0.2.2 rxjs: 7.8.2 @@ -5650,11 +5980,13 @@ snapshots: optionalDependencies: amqp-connection-manager: 5.0.0(amqplib@0.10.9) amqplib: 0.10.9 + cache-manager: 7.2.4 + ioredis: 5.8.2 - '@nestjs/platform-express@11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)': + '@nestjs/platform-express@11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)': dependencies: - '@nestjs/common': 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 express: 5.1.0 multer: 2.0.2 @@ -5685,22 +6017,51 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/testing@11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)': + '@nestjs/swagger@11.2.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 8.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.29.4 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 + + '@nestjs/terminus@11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@nestjs/microservices@11.1.8)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))': dependencies: - '@nestjs/common': 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + optionalDependencies: + '@nestjs/axios': 4.0.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(cache-manager@7.2.4)(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3))) + typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) + + '@nestjs/testing@11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)': + dependencies: + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/microservices': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8) + '@nestjs/microservices': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(amqp-connection-manager@5.0.0(amqplib@0.10.9))(amqplib@0.10.9)(cache-manager@7.2.4)(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)))': dependencies: - '@nestjs/common': 11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.8(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.8)(@nestjs/platform-express@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - typeorm: 0.3.27(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) + typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) '@noble/hashes@1.8.0': {} @@ -5732,12 +6093,32 @@ snapshots: '@pkgr/core@0.2.9': {} + '@scarf/scarf@1.4.0': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 selderee: 0.11.0 optional: true + '@sendgrid/client@8.1.6': + dependencies: + '@sendgrid/helpers': 8.0.0 + axios: 1.13.2 + transitivePeerDependencies: + - debug + + '@sendgrid/helpers@8.0.0': + dependencies: + deepmerge: 4.3.1 + + '@sendgrid/mail@8.1.6': + dependencies: + '@sendgrid/client': 8.1.6 + '@sendgrid/helpers': 8.0.0 + transitivePeerDependencies: + - debug + '@sinclair/typebox@0.34.41': {} '@sinonjs/commons@3.0.1': @@ -6189,6 +6570,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/validator@13.15.8': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.34': @@ -6461,6 +6844,8 @@ snapshots: acorn@8.15.0: {} + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -6508,6 +6893,10 @@ snapshots: buffer-more-ints: 1.0.0 url-parse: 1.5.10 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -6566,6 +6955,14 @@ snapshots: aws-ssl-profiles@1.1.2: optional: true + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + babel-jest@30.2.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -6657,6 +7054,17 @@ snapshots: bowser@2.12.1: {} + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6706,6 +7114,11 @@ snapshots: bytes@3.1.2: {} + cache-manager@7.2.4: + dependencies: + '@cacheable/utils': 2.2.0 + keyv: 5.5.4 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6757,6 +7170,8 @@ snapshots: chardet@2.1.1: {} + check-disk-space@3.4.0: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -6804,11 +7219,21 @@ snapshots: cjs-module-lexer@2.1.1: {} + class-transformer@0.5.1: {} + + class-validator@0.14.2: + dependencies: + '@types/validator': 13.15.8 + libphonenumber-js: 1.12.26 + validator: 13.15.23 + clean-css@4.2.4: dependencies: source-map: 0.6.1 optional: true + cli-boxes@2.2.1: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -6831,6 +7256,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.2: {} + co@4.6.0: {} collect-v8-coverage@1.0.3: {} @@ -6945,6 +7372,8 @@ snapshots: css-what@6.2.2: optional: true + data-uri-to-buffer@4.0.1: {} + dayjs@1.11.19: {} debug@4.4.3: @@ -6972,8 +7401,7 @@ snapshots: delayed-stream@1.0.0: {} - denque@2.1.0: - optional: true + denque@2.1.0: {} depd@2.0.0: {} @@ -7055,6 +7483,8 @@ snapshots: dotenv@16.6.1: {} + dotenv@17.2.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7308,6 +7738,8 @@ snapshots: extend-object@1.0.0: optional: true + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -7340,6 +7772,11 @@ snapshots: dependencies: bser: 2.1.1 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} file-entry-cache@8.0.0: @@ -7402,6 +7839,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -7436,6 +7875,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + formidable@3.5.4: dependencies: '@paralleldrive/cuid2': 2.3.1 @@ -7461,6 +7904,17 @@ snapshots: function-bind@1.1.2: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + generate-function@2.3.1: dependencies: is-property: 1.0.2 @@ -7635,6 +8089,13 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} iconv-lite@0.6.3: @@ -7673,6 +8134,20 @@ snapshots: ini@1.3.8: optional: true + ioredis@5.8.2: + dependencies: + '@ioredis/commands': 1.4.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + 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 + transitivePeerDependencies: + - supports-color + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} @@ -7748,6 +8223,11 @@ snapshots: isexe@2.0.0: {} + isomorphic-unfetch@4.0.2: + dependencies: + node-fetch: 3.3.2 + unfetch: 5.0.0 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@6.0.3: @@ -8191,6 +8671,10 @@ snapshots: dependencies: json-buffer: 3.0.1 + keyv@5.5.4: + dependencies: + '@keyv/serialize': 1.1.1 + leac@0.6.0: optional: true @@ -8212,6 +8696,8 @@ snapshots: libqp: 2.1.1 optional: true + libphonenumber-js@1.12.26: {} + libqp@2.1.1: optional: true @@ -8239,6 +8725,10 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -8274,6 +8764,15 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mailersend@2.6.0: + dependencies: + gaxios: 6.7.1 + isomorphic-unfetch: 4.0.2 + qs: 6.14.0 + transitivePeerDependencies: + - encoding + - supports-color + mailparser@3.9.0: dependencies: '@zone-eu/mailsplit': 5.4.7 @@ -8749,6 +9248,8 @@ snapshots: node-abort-controller@3.1.1: {} + node-domexception@1.0.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 @@ -8756,7 +9257,12 @@ snapshots: node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - optional: true + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 node-int64@0.4.0: {} @@ -9040,6 +9546,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + pug-attrs@3.0.0: dependencies: constantinople: 4.0.1 @@ -9170,6 +9678,12 @@ snapshots: readdirp@4.1.2: {} + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reflect-metadata@0.2.2: {} relateurl@0.2.7: @@ -9388,6 +9902,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + standard-as-callback@2.1.0: {} + statuses@2.0.1: {} statuses@2.0.2: {} @@ -9475,6 +9991,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: optional: true + swagger-ui-dist@5.29.4: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} synckit@0.11.11: @@ -9531,8 +10051,7 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tr46@0.0.3: - optional: true + tr46@0.0.3: {} tree-kill@1.2.2: {} @@ -9609,6 +10128,8 @@ snapshots: type-detect@4.0.8: {} + type-fest@0.20.2: {} + type-fest@0.21.3: {} type-fest@4.41.0: {} @@ -9632,7 +10153,7 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.27(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)): + typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(pg@8.16.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 3.17.0 @@ -9650,6 +10171,7 @@ snapshots: uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: + ioredis: 5.8.2 mysql2: 3.15.3 pg: 8.16.3 ts-node: 10.9.2(@types/node@22.19.0)(typescript@5.9.3) @@ -9686,6 +10208,8 @@ snapshots: undici-types@6.21.0: {} + unfetch@5.0.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -9736,8 +10260,7 @@ snapshots: uuid@11.1.0: {} - uuid@9.0.1: - optional: true + uuid@9.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -9750,6 +10273,8 @@ snapshots: valid-data-url@3.0.1: optional: true + validator@13.15.23: {} + vary@1.1.2: {} void-elements@3.1.0: @@ -9780,8 +10305,9 @@ snapshots: - encoding optional: true - webidl-conversions@3.0.1: - optional: true + web-streams-polyfill@3.3.3: {} + + webidl-conversions@3.0.1: {} webpack-node-externals@3.0.0: {} @@ -9823,7 +10349,6 @@ snapshots: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - optional: true which-typed-array@1.1.19: dependencies: @@ -9844,6 +10369,10 @@ snapshots: dependencies: isexe: 2.0.0 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + with@7.0.2: dependencies: '@babel/parser': 7.28.5 diff --git a/services/email_service/src/app.module.ts b/services/email_service/src/app.module.ts index 81f45d4..2e45baa 100644 --- a/services/email_service/src/app.module.ts +++ b/services/email_service/src/app.module.ts @@ -1,18 +1,30 @@ +import { + DatabaseModule, + CacheConfigModule, + MailerConfigModule, +} from './config'; import { Module } from '@nestjs/common'; -import { AppService } from './app.service'; +import { HttpModule } from '@nestjs/axios'; import { ConfigModule } from '@nestjs/config'; -import { AppController } from './app.controller'; -import { DatabaseModule } from './config/database.module'; -import { MailerConfigModule } from './config/mailer.module'; +import { TerminusModule } from '@nestjs/terminus'; +import { EmailModule } from './email/email.module'; +import { HealthController } from './health/health.controller'; import { DatabaseLogger } from './utils/database-logger.service'; +import { EmailStatusModule } from './email-status/email-status.module'; +import { SendGridService } from './sendgrid/sendgrid.service'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }), DatabaseModule, MailerConfigModule, + TerminusModule, + HttpModule, + CacheConfigModule, + EmailModule, + EmailStatusModule, ], - controllers: [AppController], - providers: [AppService, DatabaseLogger], + controllers: [HealthController], + providers: [DatabaseLogger, SendGridService], }) export class AppModule {} diff --git a/services/email_service/src/config/cache.module.ts b/services/email_service/src/config/cache.module.ts new file mode 100644 index 0000000..d6a447e --- /dev/null +++ b/services/email_service/src/config/cache.module.ts @@ -0,0 +1,39 @@ +import Redis from 'ioredis'; +import { Module } from '@nestjs/common'; +import { CacheModule } from '@nestjs/cache-manager'; + +@Module({ + imports: [ + CacheModule.registerAsync({ + isGlobal: true, + useFactory: () => { + if (!process.env.REDIS_URL) { + throw new Error('CacheModule: REDIS_URL is not defined!'); + } + + const redisClient = new Redis(process.env.REDIS_URL); + + // optional logging + redisClient.on('connect', () => console.log('Redis connected')); + redisClient.on('error', (err) => console.error('Redis error:', err)); + + return { + store: { + get: async (key: string): Promise => { + const val = await redisClient.get(key); + return val ? JSON.parse(val) : null; + }, + set: async (key: string, value: any, ttl: number) => { + return redisClient.set(key, JSON.stringify(value), 'EX', ttl); + }, + del: async (key: string) => { + return redisClient.del(key); + }, + }, + }; + }, + }), + ], + exports: [CacheModule], +}) +export class CacheConfigModule {} diff --git a/services/email_service/src/config/data-source.ts b/services/email_service/src/config/data-source.ts new file mode 100644 index 0000000..036d14d --- /dev/null +++ b/services/email_service/src/config/data-source.ts @@ -0,0 +1,18 @@ +import { config } from 'dotenv'; +import { DataSource } from 'typeorm'; +import { NotificationEmail } from '../email/entity/email.entity'; + +config(); + +export const AppDataSource = new DataSource({ + type: 'postgres', + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + entities: [NotificationEmail], + migrations: ['src/migrations/*.ts'], + synchronize: true, // TODO: false + logging: true, +}); diff --git a/services/email_service/src/config/database.module.ts b/services/email_service/src/config/database.module.ts index 4764c3c..7292424 100644 --- a/services/email_service/src/config/database.module.ts +++ b/services/email_service/src/config/database.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { NotificationEmail } from 'src/email/entity/email.entity'; @Module({ imports: [ @@ -8,12 +9,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ type: 'postgres', - host: configService.get('DB_HOST'), - port: +configService.get('DB_PORT'), - username: configService.get('DB_USER'), - password: configService.get('DB_PASS'), - database: configService.get('DB_NAME'), - entities: [__dirname + '/../**/*.entity{.ts,.js}'], + url: configService.get('DATABASE_URL'), + entities: [NotificationEmail], synchronize: false, logging: ['error'], extra: { max: 20, idleTimeoutMillis: 30000 }, diff --git a/services/email_service/src/config/index.ts b/services/email_service/src/config/index.ts new file mode 100644 index 0000000..265a32b --- /dev/null +++ b/services/email_service/src/config/index.ts @@ -0,0 +1,3 @@ +export * from './cache.module'; +export * from './mailer.module'; +export * from './database.module'; diff --git a/services/email_service/src/config/mailer.module.ts b/services/email_service/src/config/mailer.module.ts index 54c306e..82c9682 100644 --- a/services/email_service/src/config/mailer.module.ts +++ b/services/email_service/src/config/mailer.module.ts @@ -15,7 +15,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; pass: configService.get('MAIL_PASS'), }, }, - defaults: { from: configService.get('MAIL_FROM') }, + defaults: { from: configService.get('MAIL_USER') }, }), inject: [ConfigService], }), diff --git a/services/email_service/src/email-status/email-status.module.ts b/services/email_service/src/email-status/email-status.module.ts new file mode 100644 index 0000000..6961c04 --- /dev/null +++ b/services/email_service/src/email-status/email-status.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationEmail } from 'src/email/entity/email.entity'; +import { EmailStatusService } from './email-status.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([NotificationEmail])], + providers: [EmailStatusService], + exports: [EmailStatusService], +}) +export class EmailStatusModule {} diff --git a/services/email_service/src/email-status/email-status.service.ts b/services/email_service/src/email-status/email-status.service.ts new file mode 100644 index 0000000..6994c98 --- /dev/null +++ b/services/email_service/src/email-status/email-status.service.ts @@ -0,0 +1,39 @@ +import { Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { NotificationEmail } from '../email/entity/email.entity'; + +@Injectable() +export class EmailStatusService { + constructor( + @InjectRepository(NotificationEmail) + private readonly notificationEmailRepo: Repository, + ) {} + + async handleStatusUpdate(body: any) { + const { notification_id, status, error } = body; + + const email = await this.notificationEmailRepo.findOne({ + where: { notification_id }, + }); + if (!email) return { success: false, message: 'Notification not found' }; + + email.status = status || 'failed'; + email.error_message = error || null; + await this.notificationEmailRepo.save(email); + + return { + success: true, + data: { notification_id, status }, + message: 'Notification status updated', + meta: { + total: 1, + limit: 1, + page: 1, + total_pages: 1, + has_next: false, + has_previous: false, + }, + }; + } +} diff --git a/services/email_service/src/email/email.consumer.ts b/services/email_service/src/email/email.consumer.ts new file mode 100644 index 0000000..d3b3286 --- /dev/null +++ b/services/email_service/src/email/email.consumer.ts @@ -0,0 +1,179 @@ +import { Repository } from 'typeorm'; +import type { Cache } from 'cache-manager'; +import { Channel, ConsumeMessage } from 'amqplib'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { NotificationEmail } from './entity/email.entity'; +import { Inject, Controller, Logger } from '@nestjs/common'; +import { Ctx, EventPattern, Payload, RmqContext } from '@nestjs/microservices'; +import { SendGridService } from 'src/sendgrid/sendgrid.service'; + +interface EmailJob { + notification_id: string; + user_id: string; + user_email: string; + template_code: string; + variables: Record; + request_id: string; + priority?: number; + metadata?: Record; +} + +@Controller() +export class EmailConsumer { + private readonly logger = new Logger(EmailConsumer.name); + + constructor( + @Inject(CACHE_MANAGER) private cache: Cache, + private readonly sendGridService: SendGridService, + @InjectRepository(NotificationEmail) + private readonly notificationEmailRepo: Repository, + ) {} + + private async sendEmail(job: EmailJob) { + await this.sendGridService.sendEmail( + job.user_email, + job.template_code, + job.variables, + ); + } + + private sendToFailedQueue(channel: Channel, data: EmailJob) { + try { + const failedPayload = { + ...data, + failed_at: new Date().toISOString(), + }; + + const failedQueue = process.env.FAILED_QUEUE; + const notificationExchange = process.env.NOTIFICATIONS_EXCHANGE; + + if (!notificationExchange || !failedQueue) { + throw new Error( + 'sendToFailedQueue: NOTIFICATIONS_EXCHANGE, EMAIL_QUEUE or FAILED_QUEUE is not defined!', + ); + } + + channel.publish( + notificationExchange, + failedQueue, + Buffer.from(JSON.stringify(failedPayload)), + ); + + this.logger.warn( + `Sent failed job to DLQ: notification_id=${data.notification_id}`, + ); + } catch (err) { + this.logger.error(`Failed to publish to failed.queue: ${err}`); + } + } + + @EventPattern('email.notify') + async handleEmailNotification( + @Payload() data: EmailJob, + @Ctx() context: RmqContext, + ) { + this.logger.log('Handler triggered for email.notify'); + const channel = context.getChannelRef() as Channel; + const message = context.getMessage() as ConsumeMessage; + + this.logger.log(`Received email job: ${JSON.stringify(data)}`); + + // Validation + if ( + !data.notification_id || + !data.request_id || + !data.user_id || + !data.template_code + ) { + this.logger.error(`Invalid job payload, missing required fields.`); + channel.ack(message); // consume message + this.sendToFailedQueue(channel, data); + return; + } + + // Idempotency + const idempotencyKey = `email:sent:${data.notification_id}`; + const alreadyProcessed = await this.cache.get(idempotencyKey); + if (alreadyProcessed) { + this.logger.warn( + `Duplicate job detected, skipping notification ${data.notification_id}`, + ); + channel.ack(message); + return; + } + + // Circuit Breaker check + const circuitKey = 'email:circuit_open'; + const circuitOpen = await this.cache.get(circuitKey); + if (circuitOpen) { + this.logger.warn(`Circuit breaker open — temporarily rejecting message`); + channel.nack(message, false, true); // requeue message + return; + } + + try { + await this.sendEmail(data); + await this.notificationEmailRepo.update( + { notification_id: data.notification_id }, + { status: 'sent', sent_at: new Date() }, + ); + + await this.cache.set(idempotencyKey, true, 300); // 5 mins TTL + + channel.ack(message); + } catch (err) { + const errMessage = + err instanceof Error ? err.message : JSON.stringify(err); + + await this.notificationEmailRepo.update( + { notification_id: data.notification_id }, + { status: 'failed', error_message: errMessage }, + ); + + // Circuit Breaker increment + const failureKey = 'email:failures'; + const failures = (await this.cache.get(failureKey)) || 0; + const newCount = (Number(failures) || 0) + 1; + await this.cache.set(failureKey, newCount, 60); // expire in 60s + + if (newCount >= 5) { + this.logger.error( + 'Too many email failures! Opening circuit for 60 seconds.', + ); + await this.cache.set(circuitKey, true, 60); + } + + // Send to DLQ + this.sendToFailedQueue(channel, data); + + channel.nack(message, false, false); // remove from queue (goes to DLQ) + } + } + + @EventPattern('notification.failed') + async handleFailedEmails( + @Payload() data: EmailJob, + @Ctx() context: RmqContext, + ) { + const channel = context.getChannelRef() as Channel; + const message = context.getMessage() as ConsumeMessage; + + this.logger.warn(`Retrying failed email job: ${data.notification_id}`); + + try { + await this.sendEmail(data); + await this.notificationEmailRepo.update( + { notification_id: data.notification_id }, + { status: 'sent', sent_at: new Date() }, + ); + channel.ack(message); + } catch (err) { + const errMessage = + err instanceof Error ? err.message : JSON.stringify(err); + + this.logger.error(`Retry failed: ${errMessage}`); + channel.nack(message, false, false); // stays in DLQ + } + } +} diff --git a/services/email_service/src/email/email.controller.spec.ts b/services/email_service/src/email/email.controller.spec.ts new file mode 100644 index 0000000..9470aa8 --- /dev/null +++ b/services/email_service/src/email/email.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EmailController } from './email.controller'; + +describe('EmailController', () => { + let controller: EmailController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [EmailController], + }).compile(); + + controller = module.get(EmailController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/services/email_service/src/email/email.controller.ts b/services/email_service/src/email/email.controller.ts new file mode 100644 index 0000000..cab0225 --- /dev/null +++ b/services/email_service/src/email/email.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Post, Body } from '@nestjs/common'; +import { EmailStatusService } from '../email-status/email-status.service'; + +@Controller('api/v1/email') +export class EmailController { + constructor(private readonly emailStatusService: EmailStatusService) {} + + @Post('status') + async updateStatus(@Body() body: any) { + return this.emailStatusService.handleStatusUpdate(body); + } +} diff --git a/services/email_service/src/email/email.module.ts b/services/email_service/src/email/email.module.ts new file mode 100644 index 0000000..42581b3 --- /dev/null +++ b/services/email_service/src/email/email.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EmailConsumer } from './email.consumer'; +import { EmailController } from './email.controller'; +import { NotificationEmail } from './entity/email.entity'; +import { SendGridService } from 'src/sendgrid/sendgrid.service'; +import { RabbitInitializer } from './rabbit-initializer.service'; +import { EmailStatusModule } from 'src/email-status/email-status.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([NotificationEmail]), EmailStatusModule], + controllers: [EmailConsumer, EmailController], + providers: [RabbitInitializer, SendGridService], +}) +export class EmailModule {} diff --git a/services/email_service/src/email/entity/email.entity.ts b/services/email_service/src/email/entity/email.entity.ts new file mode 100644 index 0000000..38a5797 --- /dev/null +++ b/services/email_service/src/email/entity/email.entity.ts @@ -0,0 +1,42 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from 'typeorm'; + +export type EmailStatus = 'pending' | 'sent' | 'failed' | 'retrying'; + +@Entity('notification_email') +export class NotificationEmail { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + notification_id: string; + + @Column({ type: 'uuid' }) + user_id: string; + + @Column({ type: 'text' }) + template_id: string; + + @Column({ + type: 'enum', + enum: ['pending', 'sent', 'failed', 'retrying'], + default: 'pending', + }) + status: EmailStatus; + + @Column({ type: 'int', default: 0 }) + retry_count: number; + + @Column({ type: 'text', nullable: true }) + error_message?: string; + + @Column({ type: 'timestamp', nullable: true }) + sent_at?: Date; + + @CreateDateColumn({ type: 'timestamp' }) + created_at: Date; +} diff --git a/services/email_service/src/email/rabbit-initializer.service.ts b/services/email_service/src/email/rabbit-initializer.service.ts new file mode 100644 index 0000000..5df71f7 --- /dev/null +++ b/services/email_service/src/email/rabbit-initializer.service.ts @@ -0,0 +1,58 @@ +import { connect } from 'amqplib'; +import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; + +@Injectable() +export class RabbitInitializer implements OnModuleInit { + private readonly logger = new Logger(RabbitInitializer.name); + + async onModuleInit() { + const rabbitMqUrl = process.env.RABBITMQ_URL; + + if (!rabbitMqUrl) { + throw new Error( + 'RabbitInitializer: RABBITMQ_URL or EMAIL_QUEUE is not defined!', + ); + } + + const conn = await connect(rabbitMqUrl); + const channel = await conn.createChannel(); + + const emailQueue = process.env.EMAIL_QUEUE; + const failedQueue = process.env.FAILED_QUEUE; + const notificationExchange = process.env.NOTIFICATIONS_EXCHANGE; + + if (!notificationExchange || !emailQueue || !failedQueue) { + throw new Error( + 'RabbitInitializer: NOTIFICATIONS_EXCHANGE, EMAIL_QUEUE or FAILED_QUEUE is not defined!', + ); + } + + // Declare exchange + await channel.assertExchange(notificationExchange, 'direct', { + durable: true, + }); + + // Declare main email queue with DLX + await channel.assertQueue(emailQueue, { + durable: true, + arguments: { + 'x-dead-letter-exchange': notificationExchange, + 'x-dead-letter-routing-key': failedQueue, + }, + }); + + // Declare DLQ + await channel.assertQueue(failedQueue, { durable: true }); + + // Bind main queue to exchange with routing key + await channel.bindQueue(emailQueue, notificationExchange, 'email.notify'); + + // Bind DLQ to exchange + await channel.bindQueue(failedQueue, notificationExchange, failedQueue); + + await channel.close(); + await conn.close(); + + this.logger.log('RabbitMQ queues and bindings initialized'); + } +} diff --git a/services/email_service/src/health/health.controller.spec.ts b/services/email_service/src/health/health.controller.spec.ts new file mode 100644 index 0000000..4a26ffd --- /dev/null +++ b/services/email_service/src/health/health.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from './health.controller'; + +describe('HealthController', () => { + let controller: HealthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthController], + }).compile(); + + controller = module.get(HealthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/services/email_service/src/health/health.controller.ts b/services/email_service/src/health/health.controller.ts new file mode 100644 index 0000000..aa4d9eb --- /dev/null +++ b/services/email_service/src/health/health.controller.ts @@ -0,0 +1,49 @@ +import * as amqp from 'amqplib'; +import { ConfigService } from '@nestjs/config'; +import { Controller, Get } from '@nestjs/common'; +import { + HealthCheck, + HealthCheckService, + TypeOrmHealthIndicator, +} from '@nestjs/terminus'; + +@Controller('health') +export class HealthController { + constructor( + private config: ConfigService, + private db: TypeOrmHealthIndicator, + ) {} + + @Get() + @HealthCheck() + async check() { + const dbCheck = await this.db.pingCheck('database'); + + try { + const connection = await amqp.connect( + this.config.get('RABBITMQ_URL') as string, + ); + + await connection.close(); + + return { + status: 'ok', + info: { + ...dbCheck, + rabbitmq: { status: 'up' }, + }, + }; + } catch (err: unknown) { + const message = + err instanceof Error + ? err.message + : 'Unknown error while connecting to RabbitMQ'; + + return { + status: 'error', + info: { ...dbCheck }, + error: { rabbitmq: { status: 'down', message } }, + }; + } + } +} diff --git a/services/email_service/src/main.ts b/services/email_service/src/main.ts index 82746c7..a7fdc7d 100644 --- a/services/email_service/src/main.ts +++ b/services/email_service/src/main.ts @@ -1,36 +1,63 @@ -import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { NestFactory } from '@nestjs/core'; +import { Logger, ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { Logger } from '@nestjs/common'; async function bootstrap() { - const logger = new Logger('RabbitMQ'); - const rabbitMqUrl = process.env.RABBITMQ_URL; + const port = process.env.PORT || 3000; + const logger = new Logger('Bootstrap'); const emailQueue = process.env.EMAIL_QUEUE; + const rabbitMqUrl = process.env.RABBITMQ_URL; + const failedQueue = process.env.FAILED_QUEUE; + const notificationExchange = process.env.NOTIFICATIONS_EXCHANGE; if (!rabbitMqUrl || !emailQueue) { - throw new Error('RABBITMQ_URL or EMAIL_QUEUE is not defined in .env'); + throw new Error('Bootstrap: RABBITMQ_URL or EMAIL_QUEUE is not defined!'); } - const app = await NestFactory.createMicroservice( - AppModule, - { - transport: Transport.RMQ, - options: { - urls: [rabbitMqUrl], - queue: emailQueue, - queueOptions: { durable: true }, + const app = await NestFactory.create(AppModule); + + app.connectMicroservice({ + transport: Transport.RMQ, + options: { + urls: [rabbitMqUrl], + queue: emailQueue, + queueOptions: { + durable: true, + arguments: { + 'x-dead-letter-exchange': notificationExchange, + 'x-dead-letter-routing-key': failedQueue, + }, }, + noAck: false, + prefetchCount: 1, }, - ); - - try { - await app.listen(); - logger.log('RabbitMQ connected and Email Service is listening...'); - } catch (error) { - logger.error('RabbitMQ connection failed!', error); - process.exit(1); - } + }); + + app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + + const config = new DocumentBuilder() + .setTitle('Email Service API') + .setDescription('API documentation for the Email microservice') + .setVersion('1.0') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, document); + + await app + .startAllMicroservices() + .then(() => { + logger.log('Microservices started successfully'); + }) + .catch((error) => { + logger.error('Failed to start microservices:', error); + }); + await app.listen(port); + + logger.log(`Email Service running on: http://localhost:${port}`); + logger.log(`Swagger docs available at: http://localhost:${port}/docs`); } bootstrap(); diff --git a/services/email_service/src/migrations/1763028710799-CreateNotificationTable.ts b/services/email_service/src/migrations/1763028710799-CreateNotificationTable.ts new file mode 100644 index 0000000..27e75f5 --- /dev/null +++ b/services/email_service/src/migrations/1763028710799-CreateNotificationTable.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateNotificationTable1763028710799 implements MigrationInterface { + name = 'CreateNotificationTable1763028710799' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."notification_email_status_enum" AS ENUM('pending', 'sent', 'failed', 'retrying')`); + await queryRunner.query(`CREATE TABLE "notification_email" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "notification_id" uuid NOT NULL, "user_id" uuid NOT NULL, "template_id" text NOT NULL, "status" "public"."notification_email_status_enum" NOT NULL DEFAULT 'pending', "retry_count" integer NOT NULL DEFAULT '0', "error_message" text, "sent_at" TIMESTAMP, "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_d824f1fa65e77c6784b3fd2788a" PRIMARY KEY ("id"))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "notification_email"`); + await queryRunner.query(`DROP TYPE "public"."notification_email_status_enum"`); + } + +} diff --git a/services/email_service/src/sendgrid/sendgrid.service.spec.ts b/services/email_service/src/sendgrid/sendgrid.service.spec.ts new file mode 100644 index 0000000..daced11 --- /dev/null +++ b/services/email_service/src/sendgrid/sendgrid.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SendgridService } from './sendgrid.service'; + +describe('SendgridService', () => { + let service: SendgridService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SendgridService], + }).compile(); + + service = module.get(SendgridService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/services/email_service/src/sendgrid/sendgrid.service.ts b/services/email_service/src/sendgrid/sendgrid.service.ts new file mode 100644 index 0000000..3f6f694 --- /dev/null +++ b/services/email_service/src/sendgrid/sendgrid.service.ts @@ -0,0 +1,39 @@ +import { Injectable, Logger } from '@nestjs/common'; +import sendgridMail from '@sendgrid/mail'; // Correct import +import { config } from 'dotenv'; + +config(); + +@Injectable() +export class SendGridService { + private readonly logger = new Logger(SendGridService.name); + + constructor() { + sendgridMail.setApiKey(process.env.SENDGRID_API_KEY as string); + } + + async sendEmail( + user_email: string, + template_code: string, + variables?: Record, + ) { + const message = { + to: user_email, + from: process.env.MAIL_USER as string, + subject: `Notification: ${template_code}`, + text: `Hello ${variables?.name || 'User'}, Your notification "${template_code}" has been triggered.`, + html: `

Hello ${variables?.name || 'User'},

Your notification "${template_code}" has been triggered.

`, + }; + + try { + this.logger.log('Sending email with params:', message); + await sendgridMail.send(message); + this.logger.log(`Email sent successfully to ${user_email}`); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : JSON.stringify(err); + this.logger.error(`Failed to send email: ${errorMessage}`); + throw err; + } + } +} diff --git a/services/email_service/src/utils/database-logger.service.ts b/services/email_service/src/utils/database-logger.service.ts index d7787b5..a903dad 100644 --- a/services/email_service/src/utils/database-logger.service.ts +++ b/services/email_service/src/utils/database-logger.service.ts @@ -1,5 +1,5 @@ -import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { DataSource } from 'typeorm'; +import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; @Injectable() export class DatabaseLogger implements OnModuleInit {