diff --git a/docker-compose.yaml b/docker-compose.yaml index 23a1dd1..a7d0b39 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,6 +16,17 @@ services: interval: 5s timeout: 5s retries: 5 - + rabbitmq: + image: rabbitmq:3-management + container_name: local_rabbitmq + restart: always + ports: + - '5672:5672' # AMQP protocol + - '15672:15672' # Management UI + healthcheck: + test: ['CMD', 'rabbitmqctl', 'status'] + interval: 10s + timeout: 10s + retries: 5 volumes: postgres_data: diff --git a/package-lock.json b/package-lock.json index 189800d..df14afb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,11 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/microservices": "^11.1.6", "@nestjs/platform-express": "^11.0.1", "@prisma/client": "^6.14.0", + "amqp-connection-manager": "^4.1.14", + "amqplib": "^0.10.9", "axios": "^1.11.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -1012,6 +1015,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz", + "integrity": "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", @@ -1060,15 +1073,15 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.2.tgz", + "integrity": "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==", "dev": true, "license": "MIT", "dependencies": { + "@inquirer/ansi": "^1.0.0", "@inquirer/figures": "^1.0.13", "@inquirer/type": "^3.0.8", - "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -1088,15 +1101,15 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", - "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.20.tgz", + "integrity": "sha512-7omh5y5bK672Q+Brk4HBbnHNowOZwrb/78IFXdrEB9PfdxL3GudQyDk8O9vQ188wj3xrEebS2M9n18BjJoI83g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8", - "external-editor": "^3.1.0" + "@inquirer/core": "^10.2.2", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=18" @@ -1133,6 +1146,45 @@ } } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@inquirer/figures": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", @@ -2386,6 +2438,64 @@ } } }, + "node_modules/@nestjs/microservices": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.1.6.tgz", + "integrity": "sha512-5ibvCvZ8IBxKhpSkyAC+EBdtm0XVAu3h+GV4PmOi4bXbRvVBT3Kwqq4td2wkWDN3VqteChjQ0aTADv0jizhCrg==", + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.5", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz", @@ -3862,6 +3972,35 @@ "ajv": "^6.9.1" } }, + "node_modules/amqp-connection-manager": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/amqp-connection-manager/-/amqp-connection-manager-4.1.14.tgz", + "integrity": "sha512-1km47dIvEr0HhMUazqovSvNwIlSvDX2APdUpULaINtHpiki1O+cLRaTeXb/jav4OLtH+k6GBXx5gsKOT9kcGKQ==", + "license": "MIT", + "dependencies": { + "promise-breaker": "^6.0.0" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">5.0.0" + }, + "peerDependencies": { + "amqplib": "*" + } + }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3995,9 +4134,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -4274,6 +4413,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4420,9 +4565,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "dev": true, "license": "MIT" }, @@ -5552,34 +5697,6 @@ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "license": "MIT" }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -8234,16 +8351,6 @@ "node": ">=8" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8634,6 +8741,12 @@ } } }, + "node_modules/promise-breaker": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/promise-breaker/-/promise-breaker-6.0.0.tgz", + "integrity": "sha512-BthzO9yTPswGf7etOBiHCVuugs2N01/Q/94dIPls48z2zCmrnDptUUZzfIb+41xq0MnYZ/BzmOd6ikDR4ibNZA==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8695,6 +8808,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8830,6 +8949,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -9860,19 +9985,6 @@ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", "license": "MIT" }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -10354,6 +10466,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 8a7efe5..8be54f5 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,11 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/microservices": "^11.1.6", "@nestjs/platform-express": "^11.0.1", "@prisma/client": "^6.14.0", + "amqp-connection-manager": "^4.1.14", + "amqplib": "^0.10.9", "axios": "^1.11.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", diff --git a/prisma/migrations/20250916103304_gateway_transaction_status_enum/migration.sql b/prisma/migrations/20250916103304_gateway_transaction_status_enum/migration.sql new file mode 100644 index 0000000..f582e5e --- /dev/null +++ b/prisma/migrations/20250916103304_gateway_transaction_status_enum/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "public"."TransactionStatus" ADD VALUE 'GATEWAY'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 856daea..a9a5785 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -64,4 +64,5 @@ enum TransactionStatus { COMPLETED FAILED CANCELLED + GATEWAY } diff --git a/src/app.module.ts b/src/app.module.ts index eba1294..35a7c75 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { HealthModule } from './modules/health/health.module'; +import { RabbitModule } from './modules/rabbitMQ/rabbit.module'; import { TransactionModule } from './modules/transaction/transaction.module'; import { WalletModule } from './modules/wallet/wallet.module'; import { CurrencyClientModule } from './shared/clients/currencyExchange/currency.module'; import { PrismaModule } from './shared/database/prisma.module'; import { LoggerModule } from './shared/logger/logger.module'; + @Module({ imports: [ HealthModule, @@ -17,6 +19,7 @@ import { LoggerModule } from './shared/logger/logger.module'; ConfigModule.forRoot({ isGlobal: true, }), + RabbitModule, ], }) export class AppModule {} diff --git a/src/main.ts b/src/main.ts index 25fced3..cece8d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,25 @@ import { NestFactory } from '@nestjs/core'; +import { Transport } from '@nestjs/microservices'; import { AppModule } from './app.module'; +import { RabbitQueues } from './modules/rabbitMQ/rabbit.enum'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + app.connectMicroservice({ + transport: Transport.RMQ, + options: { + urls: [process.env.RABBIT_MQ_URL || 'amqp://localhost:5672'], + queue: RabbitQueues.RES, + queueOptions: { + durable: true, + }, + }, + }); + + await app.startAllMicroservices(); await app.listen(process.env.PORT ?? 3000); } +// eslint-disable-next-line @typescript-eslint/no-floating-promises bootstrap(); diff --git a/src/modules/gateway/app/index.ts b/src/modules/gateway/app/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/gateway/domain/gateway.service.ts b/src/modules/gateway/domain/gateway.service.ts index 5332de0..ce8788b 100644 --- a/src/modules/gateway/domain/gateway.service.ts +++ b/src/modules/gateway/domain/gateway.service.ts @@ -3,10 +3,10 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AxiosResponse } from 'axios'; import { firstValueFrom } from 'rxjs'; -import { jsonStringifyReplacer } from 'src/shared/utils/json.utils'; import { WebhookPayload } from '../../../modules/transaction/app/input'; import { EnvVariables } from '../../../shared/config/envEnums'; import { AppLoggerService } from '../../../shared/logger/app-logger.service'; +import { jsonStringifyReplacer } from '../../../shared/utils/json.utils'; import { GatewayOutput, GatewayResult } from '../api/output'; @Injectable() diff --git a/src/modules/gateway/domain/index.ts b/src/modules/gateway/domain/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts index c999e59..4928f87 100644 --- a/src/modules/health/health.module.ts +++ b/src/modules/health/health.module.ts @@ -1,11 +1,9 @@ import { Module } from '@nestjs/common'; -import { AppLoggerService } from 'src/shared/logger/app-logger.service'; -import { LoggerModule } from 'src/shared/logger/logger.module'; +import { LoggerModule } from '../../shared/logger/logger.module'; import { HealthController } from './interfaces/controllers/health.controller'; @Module({ imports: [LoggerModule], - providers: [AppLoggerService], controllers: [HealthController], }) export class HealthModule {} diff --git a/src/modules/rabbitMQ/rabbit.enum.ts b/src/modules/rabbitMQ/rabbit.enum.ts new file mode 100644 index 0000000..90d715b --- /dev/null +++ b/src/modules/rabbitMQ/rabbit.enum.ts @@ -0,0 +1,8 @@ +export enum RabbitQueues { + REQ = 'transactions.request', + RES = 'transactions.response', +} + +export enum RabbitClientNames { + TRANSACTION_CLIENT = 'TRANSACTION_CLIENT', +} diff --git a/src/modules/rabbitMQ/rabbit.module.ts b/src/modules/rabbitMQ/rabbit.module.ts new file mode 100644 index 0000000..7f6db6b --- /dev/null +++ b/src/modules/rabbitMQ/rabbit.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { RabbitClientNames, RabbitQueues } from './rabbit.enum'; +import { TransactionRabbitService } from './services/transaction.rabbit.service'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: RabbitClientNames.TRANSACTION_CLIENT, + transport: Transport.RMQ, + options: { + urls: [process.env.RABBIT_MQ_URL || 'amqp://localhost:5672'], + queue: RabbitQueues.REQ, + queueOptions: { + durable: true, + }, + }, + }, + ]), + ], + providers: [TransactionRabbitService], + exports: [TransactionRabbitService], +}) +export class RabbitModule {} diff --git a/src/modules/rabbitMQ/services/interfaces/transaction.request.event.input.ts b/src/modules/rabbitMQ/services/interfaces/transaction.request.event.input.ts new file mode 100644 index 0000000..afe2f8a --- /dev/null +++ b/src/modules/rabbitMQ/services/interfaces/transaction.request.event.input.ts @@ -0,0 +1,7 @@ +export interface TransactionEvent { + id: number; + amount: number; + currency: string; + status: string; + originCreatedAt: Date; +} diff --git a/src/modules/rabbitMQ/services/transaction.rabbit.service.ts b/src/modules/rabbitMQ/services/transaction.rabbit.service.ts new file mode 100644 index 0000000..d4e8757 --- /dev/null +++ b/src/modules/rabbitMQ/services/transaction.rabbit.service.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { TransactionOutput } from '../../../modules/transaction/app/complete-transaction-use-case/output'; +import { AppLoggerService } from '../../../shared/logger/app-logger.service'; +import { jsonStringifyReplacer } from '../../../shared/utils/json.utils'; +import { Transaction } from '../../transaction/domain/transaction.entity'; +import { RabbitClientNames, RabbitQueues } from '../rabbit.enum'; +import { TransactionEvent } from './interfaces/transaction.request.event.input'; + +@Injectable() +export class TransactionRabbitService implements OnModuleInit { + constructor( + @Inject(RabbitClientNames.TRANSACTION_CLIENT) private client: ClientProxy, + private readonly logger: AppLoggerService, + ) {} + + private get logPrefix(): string { + return `[${this.constructor.name}]`; + } + + async onModuleInit() { + await this.client.connect(); + } + + sendTrxRequest(transaction: Transaction): TransactionEvent { + try { + const eventInput = this.mapTransactionToEvent(transaction); + this.client.emit(RabbitQueues.REQ, { + eventInput, + }); + this.logger.log( + this.logPrefix, + `Successfully sent an event ${JSON.stringify(eventInput)} to "${RabbitQueues.REQ}" queue using ${RabbitClientNames.TRANSACTION_CLIENT} client.`, + ); + return eventInput; + } catch (error) { + this.logger.error( + this.logPrefix, + `Error sending transaction complete message for transactionId: ${transaction.id}`, + (error as Error).stack, + ); + throw error; + } + } + + async ackTransactionResponse(event: TransactionOutput) { + this.logger.debug( + this.logPrefix, + `Processed transaction response for transaction: ${JSON.stringify(event, jsonStringifyReplacer)}. Updated wallet balance: ${Number(event.funds?.currentBalance)}`, + ); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + private mapTransactionToEvent(transaction: Transaction): TransactionEvent { + const input: TransactionEvent = { + id: transaction.id, + amount: Number(transaction.amount), + currency: transaction.currentCurrency, + status: transaction.status, + originCreatedAt: transaction.clientTransactionDate + ? transaction.clientTransactionDate + : new Date(), + }; + return input; + } +} diff --git a/src/modules/transaction/api/representation.ts b/src/modules/transaction/api/representation.ts index b1b204f..0e88e1f 100644 --- a/src/modules/transaction/api/representation.ts +++ b/src/modules/transaction/api/representation.ts @@ -1,4 +1,4 @@ -import { FundsRepresentation } from 'src/modules/wallet/api/representation'; +import { FundsRepresentation } from '../../../modules/wallet/api/representation'; import { CurrencyEnum } from '../../../shared/validations/currency'; import { TransactionStatusEnum } from '../../../shared/validations/transaction/status'; import { TransactionTypeEnum } from '../../../shared/validations/transaction/type'; diff --git a/src/modules/transaction/api/transaction.controller.ts b/src/modules/transaction/api/transaction.controller.ts index 2f147cd..2698861 100644 --- a/src/modules/transaction/api/transaction.controller.ts +++ b/src/modules/transaction/api/transaction.controller.ts @@ -12,20 +12,24 @@ import { Post, ValidationPipe, } from '@nestjs/common'; -import { GatewayOutput } from 'src/modules/gateway/api/output'; -import { TransactionWebhookDto } from 'src/shared/dto/transaction-webhook-payload.dto'; +import { GatewayOutput } from '../../../modules/gateway/api/output'; import { FundsRepresentation } from '../../../modules/wallet/api/representation'; import { CreateTransactionDto } from '../../../shared/dto/create-transaction.dto'; +import { TransactionWebhookDto } from '../../../shared/dto/transaction-webhook-payload.dto'; import { AppLoggerService } from '../../../shared/logger/app-logger.service'; import { ApiRoutes, TransactionRoutes } from '../../../shared/router/routes'; import { jsonStringifyReplacer } from '../../../shared/utils/json.utils'; +import { TransactionEvent } from '../../rabbitMQ/services/interfaces/transaction.request.event.input'; import { CancelTransactionUseCase } from '../app/cancel-transaction-use-case/cancel-transaction.use-case'; import { CompleteTransactionUseCase } from '../app/complete-transaction-use-case/complete-transaction.use-case'; import { CreateTransactionUseCase } from '../app/create-transaction-use-case/create-transaction.use-case'; import { CreateTransactionInput } from '../app/input'; -import { UpdateTransactionFromWebhookUseCase } from '../app/update-transaction-from-webhook-use-case/update-transaction-from-webhook.use-case'; +import { SendCompleteTransactionEventUseCase } from '../app/send-complete-trx-event.use-case/send-complete-trx-event.use-case'; +import { UpdateTransactionGatewayResponseUseCase } from '../app/update-transaction-gateway-response-use-case/update-transaction-gateway-response.use-case'; +import { UpdateSourceEnum } from '../app/update-transaction-gateway-response-use-case/update.source.enum'; import { TransactionService } from '../domain/services/transaction.service'; import { Transaction } from '../domain/transaction.entity'; +import TransactionMapper from '../mappers/transaction.mapper'; import { TransactionRepresentation } from './representation'; import { TransactionRepresentationMapper } from './representationMapper'; @@ -37,11 +41,13 @@ export class TransactionController { constructor( private readonly logger: AppLoggerService, private readonly transactionService: TransactionService, + private readonly transactionRepresentationMapper: TransactionRepresentationMapper, private readonly createTransactionUseCase: CreateTransactionUseCase, private readonly completeTransactionUseCase: CompleteTransactionUseCase, - private readonly transactionRepresentationMapper: TransactionRepresentationMapper, private readonly cancelTransactionUseCase: CancelTransactionUseCase, - private readonly updateTransactionFromWebhookUseCase: UpdateTransactionFromWebhookUseCase, + private readonly updateTrxGatewayResponseUseCase: UpdateTransactionGatewayResponseUseCase, + private readonly sendCompleteTransactionEventUseCase: SendCompleteTransactionEventUseCase, + private readonly transactionMapper: TransactionMapper, ) {} @Post() @@ -137,15 +143,19 @@ export class TransactionController { }, }), ) - input: TransactionWebhookDto, + webhookDto: TransactionWebhookDto, ): Promise { try { this.logger.log( this.logPrefix, - `Webhook called with input: ${JSON.stringify(input, jsonStringifyReplacer)}`, + `Webhook called with input: ${JSON.stringify(webhookDto, jsonStringifyReplacer)}`, + ); + const input = + this.transactionMapper.fromWebhookToUpdateTrxInput(webhookDto); + const transaction = await this.updateTrxGatewayResponseUseCase.run( + input, + UpdateSourceEnum.WEBHOOK, ); - const transaction = - await this.updateTransactionFromWebhookUseCase.run(input); this.logger.log( this.logPrefix, `Processes transaction: ${JSON.stringify(transaction, jsonStringifyReplacer)}`, @@ -161,4 +171,14 @@ export class TransactionController { throw error; } } + + @Post(TransactionRoutes.COMPLETE_TRX_EVENTS) + @HttpCode(HttpStatus.OK) + async rabbitTest( + @Param('transactionId', ParseIntPipe) transactionId: number, + ): Promise { + const result = + await this.sendCompleteTransactionEventUseCase.run(transactionId); + return result; + } } diff --git a/src/modules/transaction/app/complete-transaction-use-case/complete-transaction.use-case.ts b/src/modules/transaction/app/complete-transaction-use-case/complete-transaction.use-case.ts index 1d21be2..c5dfaf7 100644 --- a/src/modules/transaction/app/complete-transaction-use-case/complete-transaction.use-case.ts +++ b/src/modules/transaction/app/complete-transaction-use-case/complete-transaction.use-case.ts @@ -72,6 +72,14 @@ export class CompleteTransactionUseCase { this.logger.error(this.logPrefix, err); throw new BadGatewayException(err); } + const updatedTransaction = await this.transactionService.update({ + transactionId: transaction.id, + status: TransactionStatusEnum.GATEWAY, + }); + this.logger.log( + this.logPrefix, + `Updating status of transaction: ${updatedTransaction.id} from ${transaction.status} to ${updatedTransaction.status}`, + ); return gatewayResponse; } diff --git a/src/modules/transaction/app/complete-transaction-use-case/output.ts b/src/modules/transaction/app/complete-transaction-use-case/output.ts index e734f98..6fb2f9f 100644 --- a/src/modules/transaction/app/complete-transaction-use-case/output.ts +++ b/src/modules/transaction/app/complete-transaction-use-case/output.ts @@ -1,4 +1,4 @@ -import { FundsInWallet } from 'src/modules/wallet/app/output'; +import { FundsInWallet } from '../../../../modules/wallet/app/output'; import { Transaction } from '../../domain/transaction.entity'; export interface TransactionOutput { diff --git a/src/modules/transaction/app/send-complete-trx-event.use-case/send-complete-trx-event.use-case.ts b/src/modules/transaction/app/send-complete-trx-event.use-case/send-complete-trx-event.use-case.ts new file mode 100644 index 0000000..2381d33 --- /dev/null +++ b/src/modules/transaction/app/send-complete-trx-event.use-case/send-complete-trx-event.use-case.ts @@ -0,0 +1,109 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { WalletService } from '../../../../modules/wallet/app/services/app-wallet.service'; +import { AppLoggerService } from '../../../../shared/logger/app-logger.service'; +import { jsonStringifyReplacer } from '../../../../shared/utils/json.utils'; +import { TransactionStatusEnum } from '../../../../shared/validations/transaction/status'; +import { TransactionEvent } from '../../../rabbitMQ/services/interfaces/transaction.request.event.input'; +import { TransactionRabbitService } from '../../../rabbitMQ/services/transaction.rabbit.service'; +import { TransactionService } from '../../domain/services/transaction.service'; +import { Transaction } from '../../domain/transaction.entity'; + +@Injectable() +export class SendCompleteTransactionEventUseCase { + private get logPrefix(): string { + return `[${this.constructor.name}] - `; + } + + constructor( + private readonly appLogger: AppLoggerService, + private readonly transactionService: TransactionService, + private readonly rabbitService: TransactionRabbitService, + private readonly walletService: WalletService, + ) { + this.appLogger.log(this.logPrefix, `Object initialized.`); + } + + /** + * This TypeScript function asynchronously processes a transaction by updating its status and sending + * a request using RabbitMQ. + * @param {number} transactionId - The `transactionId` parameter is a unique identifier for a + * specific transaction. It is used to retrieve the transaction details from the transaction service, + * check the status of the transaction, and update the status of the transaction to + * `TransactionStatusEnum.GATEWAY` once certain conditions are met in the `run + * @returns The `run` method is returning a `TransactionEvent` object. + */ + public async run(transactionId: number): Promise { + const transaction = await this.transactionService.getById(transactionId); + this.appLogger.debug( + this.logPrefix, + `Found transaction: ${JSON.stringify(transaction, jsonStringifyReplacer)}`, + ); + if (transaction.status !== TransactionStatusEnum.PENDING) { + this.processNotPendingTransaction(transaction); + } + const sendEvent = await this.checkWalletBalance(transaction); + + if (!sendEvent) { + return this.returnFailedEvent(transaction); + } + + const eventSent = this.rabbitService.sendTrxRequest(transaction); + const updatedTransaction = await this.transactionService.update({ + transactionId: transaction.id, + status: TransactionStatusEnum.GATEWAY, + }); + this.logUpdate(transaction, updatedTransaction); + return eventSent; + } + + private async checkWalletBalance(transaction: Transaction): Promise { + const wallet = await this.walletService.getByWalletId(transaction.walletId); + const newBalance = wallet.balance + transaction.amount; + if (newBalance > 0) { + this.appLogger.log( + this.logPrefix, + `Sufficient balance to perform transaction: ${transaction.id}. Proceeding to emit event.`, + ); + return true; + } + const updatedTransaction = await this.transactionService.update({ + transactionId: transaction.id, + status: TransactionStatusEnum.FAILED, + }); + this.logUpdate( + transaction, + updatedTransaction, + `Not sufficient balance on wallet: ${wallet.id}`, + ); + return false; + } + + private logUpdate( + transaction: Transaction, + updatedTransaction: Transaction, + msg?: string, + ): void { + this.appLogger.log( + this.logPrefix, + `${msg} Updating status of transaction: ${updatedTransaction.id} from ${transaction.status} to ${updatedTransaction.status}`, + ); + } + + private returnFailedEvent(transaction: Transaction): TransactionEvent { + return { + id: transaction.id, + amount: Number(transaction.amount), + currency: transaction.currentCurrency, + status: TransactionStatusEnum.FAILED, + originCreatedAt: transaction.clientTransactionDate + ? transaction.clientTransactionDate + : new Date(), + }; + } + + private processNotPendingTransaction(transaction: Transaction): void { + const err = `Transaction: ${transaction.id} - "${transaction.status}" is not ${TransactionStatusEnum.PENDING}. Unable to complete transaction.`; + this.appLogger.warn(this.logPrefix, err); + throw new BadRequestException(err); + } +} diff --git a/src/modules/transaction/app/update-transaction-from-webhook-use-case/update-transaction-from-webhook.use-case.ts b/src/modules/transaction/app/update-transaction-from-webhook-use-case/update-transaction-from-webhook.use-case.ts deleted file mode 100644 index 87cab9a..0000000 --- a/src/modules/transaction/app/update-transaction-from-webhook-use-case/update-transaction-from-webhook.use-case.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { WalletService } from '../../../../modules/wallet/app/services/app-wallet.service'; -import { TransactionWebhookDto } from '../../../../shared/dto/transaction-webhook-payload.dto'; -import { AppLoggerService } from '../../../../shared/logger/app-logger.service'; -import { TransactionStatusEnum } from '../../../../shared/validations/transaction/status'; -import { TransactionService } from '../../domain/services/transaction.service'; -import { TransactionOutput } from '../complete-transaction-use-case/output'; -import { UpdateTransactionInput } from '../input'; - -@Injectable() -export class UpdateTransactionFromWebhookUseCase { - private get logPrefix(): string { - return `[${this.constructor.name}] - `; - } - constructor( - private readonly appLogger: AppLoggerService, - private readonly service: TransactionService, - private readonly walletService: WalletService, - ) {} - - /** - * This TypeScript function updates a transaction based on a webhook payload and handles balance - * updates for a wallet. - * @param {TransactionWebhookDto} webhookPayload - The `webhookPayload` parameter in the `run` function - * represents the data received from a webhook. It contains information about a transaction, such as - * the transaction ID and status. - * @returns The `run` function returns a `TransactionOutput` object, which contains two properties: - * 1. `transaction`: This property contains the updated transaction object after processing the - * webhook payload. - * 2. `funds`: This property contains the updated funds in the wallet if the transaction status is - * `COMPLETED`, otherwise it is `null`. - */ - public async run( - webhookPayload: TransactionWebhookDto, - ): Promise { - // Here we only receive the transaction with sufficient balance which can be failed only at Gateway - this.appLogger.log( - this.logPrefix, - `Updating transaction from webhook: ${JSON.stringify(webhookPayload)}`, - ); - const transactionId: number = webhookPayload.id; - const transactionInput = await this.service.getById(transactionId); - - const wallet = await this.walletService.getByWalletId( - transactionInput.walletId, - ); - - const updateTransactionInput: UpdateTransactionInput = { - transactionId: transactionId, - status: webhookPayload.status, - }; - - const transaction = await this.service.update(updateTransactionInput); - - if (transaction.status === TransactionStatusEnum.COMPLETED) { - const funds = await this.walletService.updateBalance( - wallet.tokenId, - transaction.amount, - transaction.currentCurrency, - ); - return { transaction: transaction, funds }; - } - return { transaction: transaction, funds: null }; - } -} diff --git a/src/modules/transaction/app/update-transaction-gateway-response-use-case/update-transaction-gateway-response.use-case.ts b/src/modules/transaction/app/update-transaction-gateway-response-use-case/update-transaction-gateway-response.use-case.ts new file mode 100644 index 0000000..f9a1ea3 --- /dev/null +++ b/src/modules/transaction/app/update-transaction-gateway-response-use-case/update-transaction-gateway-response.use-case.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; +import { AppLoggerService } from '../../../../shared/logger/app-logger.service'; +import { TransactionStatusEnum } from '../../../../shared/validations/transaction/status'; +import { WalletService } from '../../../wallet/app/services/app-wallet.service'; +import { TransactionService } from '../../domain/services/transaction.service'; +import { TransactionOutput } from '../complete-transaction-use-case/output'; +import { UpdateTransactionInput } from '../input'; +import { UpdateSourceEnum } from './update.source.enum'; + +@Injectable() +export class UpdateTransactionGatewayResponseUseCase { + private get logPrefix(): string { + return `[${this.constructor.name}] - `; + } + constructor( + private readonly appLogger: AppLoggerService, + private readonly transactionService: TransactionService, + private readonly walletService: WalletService, + ) {} + + /** + * This TypeScript function updates a transaction based on input data from a webhook and handles + * balance updates in a wallet. + * @param {UpdateTransactionInput} input - The `run` function you provided is an asynchronous + * function that updates a transaction based on the input received. The `input` parameter is of type + * `UpdateTransactionInput`, which contains information about the transaction to be updated. The + * function first logs the details of the input, retrieves the transaction and wallet information + * @returns The `run` function returns a `TransactionOutput` object, which contains the updated + * transaction and, if the transaction status is `COMPLETED`, the updated funds in the wallet. If the + * transaction status is not `COMPLETED`, the `funds` property in the output will be `null`. + */ + public async run( + input: UpdateTransactionInput, + source: UpdateSourceEnum, + ): Promise { + this.appLogger.log( + this.logPrefix, + `Updating transaction from: ${source}: ${JSON.stringify(input)}`, + ); + const transactionId: number = input.transactionId; + const transactionInput = + await this.transactionService.getById(transactionId); + + const wallet = await this.walletService.getByWalletId( + transactionInput.walletId, + ); + + const updateTransactionInput: UpdateTransactionInput = { + transactionId: transactionId, + status: input.status, + }; + + const transaction = await this.transactionService.update( + updateTransactionInput, + ); + + if (transaction.status === TransactionStatusEnum.COMPLETED) { + const funds = await this.walletService.updateBalance( + wallet.tokenId, + transaction.amount, + transaction.currentCurrency, + ); + return { transaction: transaction, funds }; + } + return { transaction: transaction, funds: null }; + } +} diff --git a/src/modules/transaction/app/update-transaction-gateway-response-use-case/update.source.enum.ts b/src/modules/transaction/app/update-transaction-gateway-response-use-case/update.source.enum.ts new file mode 100644 index 0000000..80c4727 --- /dev/null +++ b/src/modules/transaction/app/update-transaction-gateway-response-use-case/update.source.enum.ts @@ -0,0 +1,4 @@ +export enum UpdateSourceEnum { + WEBHOOK = 'WEBHOOK', + RABBIT = 'RabbitMQ', +} diff --git a/src/modules/transaction/domain/transaction.entity.ts b/src/modules/transaction/domain/transaction.entity.ts index f14da17..3a70175 100644 --- a/src/modules/transaction/domain/transaction.entity.ts +++ b/src/modules/transaction/domain/transaction.entity.ts @@ -76,6 +76,7 @@ export class Transaction { } return status as TransactionStatusType; } + private static validateCurrency(currency: string): CurrencyType { if (!CURRENCY_TYPE.includes(currency as CurrencyType)) { throw new Error( diff --git a/src/modules/transaction/infra/repo/transaction.postgres.repo.impl.ts b/src/modules/transaction/infra/repo/transaction.postgres.repo.impl.ts index f839fa8..861057d 100644 --- a/src/modules/transaction/infra/repo/transaction.postgres.repo.impl.ts +++ b/src/modules/transaction/infra/repo/transaction.postgres.repo.impl.ts @@ -7,6 +7,7 @@ import { jsonStringifyReplacer } from '../../../../shared/utils/json.utils'; import { TransactionInput, UpdateTransactionInput } from '../../app/input'; import { Transaction } from '../../domain/transaction.entity'; import { TransactionRepository } from '../../domain/transaction.repo'; + @Injectable() export class TransactionRepoImpl implements TransactionRepository { private get logPrefix(): string { diff --git a/src/modules/transaction/interfaces/rabbit.controller.ts b/src/modules/transaction/interfaces/rabbit.controller.ts new file mode 100644 index 0000000..26d541e --- /dev/null +++ b/src/modules/transaction/interfaces/rabbit.controller.ts @@ -0,0 +1,36 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; +import type { TransactionEvent } from '../../../modules/rabbitMQ/services/interfaces/transaction.request.event.input'; +import { TransactionRabbitService } from '../../../modules/rabbitMQ/services/transaction.rabbit.service'; +import { AppLoggerService } from '../../../shared/logger/app-logger.service'; +import { RabbitQueues } from '../../rabbitMQ/rabbit.enum'; +import { UpdateTransactionGatewayResponseUseCase } from '../app/update-transaction-gateway-response-use-case/update-transaction-gateway-response.use-case'; +import { UpdateSourceEnum } from '../app/update-transaction-gateway-response-use-case/update.source.enum'; +import TransactionMapper from '../mappers/transaction.mapper'; + +@Controller() +export class RabbitController { + private get logPrefix(): string { + return `[${this.constructor.name}]`; + } + constructor( + private readonly appLogger: AppLoggerService, + private readonly rabbitService: TransactionRabbitService, + private readonly updateTrxGatewayResponseUseCase: UpdateTransactionGatewayResponseUseCase, + private readonly mapper: TransactionMapper, + ) {} + + @EventPattern(RabbitQueues.RES) + async handleTransactionResponse(@Payload() data: TransactionEvent) { + this.appLogger.log( + this.logPrefix, + `Starting processing TransactionEvent: ${JSON.stringify(data)}`, + ); + const input = this.mapper.fromEventToUpdateTrxInput(data); + const result = await this.updateTrxGatewayResponseUseCase.run( + input, + UpdateSourceEnum.RABBIT, + ); + await this.rabbitService.ackTransactionResponse(result); + } +} diff --git a/src/modules/transaction/mappers/transaction.mapper.ts b/src/modules/transaction/mappers/transaction.mapper.ts new file mode 100644 index 0000000..967844b --- /dev/null +++ b/src/modules/transaction/mappers/transaction.mapper.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { TransactionEvent } from '../../../modules/rabbitMQ/services/interfaces/transaction.request.event.input'; +import { TransactionWebhookDto } from '../../../shared/dto/transaction-webhook-payload.dto'; +import { AppLoggerService } from '../../../shared/logger/app-logger.service'; +import { + TRANSACTION_STATUS_TYPE, + TransactionStatusEnum, + TransactionStatusType, + validTransactionStatuses, +} from '../../../shared/validations/transaction/status'; +import { UpdateTransactionInput } from '../app/input'; + +@Injectable() +export default class TransactionMapper { + private get logPrefix(): string { + return `[${this.constructor.name}]`; + } + + constructor(private readonly appLogger: AppLoggerService) {} + + public fromWebhookToUpdateTrxInput( + webhookDto: TransactionWebhookDto, + ): UpdateTransactionInput { + this.appLogger.log( + this.logPrefix, + `Converting TransactionWebhookDto: ${JSON.stringify(webhookDto)} to UpdateTransactionInput`, + ); + return { + transactionId: webhookDto.id, + status: webhookDto.status, + }; + } + + public fromEventToUpdateTrxInput( + event: TransactionEvent, + ): UpdateTransactionInput { + this.appLogger.log( + this.logPrefix, + `Converting TransactionEvent: ${JSON.stringify(event)} to UpdateTransactionInput`, + ); + const statusEnum = this.convertStatusToEnum(event.status); + this.appLogger.log(this.logPrefix, `FOUND ENUM: ${statusEnum}`); + return { + transactionId: event.id, + status: statusEnum, + }; + } + + private convertStatusToEnum(status: string): TransactionStatusEnum { + if (!TRANSACTION_STATUS_TYPE.includes(status as TransactionStatusType)) { + throw new Error( + `Invalid transaction status: ${status}. Allowed transaction types: ${JSON.stringify(validTransactionStatuses)}`, + ); + } + return status as TransactionStatusEnum; + } +} diff --git a/src/modules/transaction/transaction.module.ts b/src/modules/transaction/transaction.module.ts index 7d59d77..2e7c467 100644 --- a/src/modules/transaction/transaction.module.ts +++ b/src/modules/transaction/transaction.module.ts @@ -1,20 +1,24 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '../../shared/logger/logger.module'; import { GatewayModule } from '../gateway/gateway.module'; +import { RabbitModule } from '../rabbitMQ/rabbit.module'; import { WalletModule } from '../wallet/wallet.module'; import { TransactionRepresentationMapper } from './api/representationMapper'; import { TransactionController } from './api/transaction.controller'; import { CancelTransactionUseCase } from './app/cancel-transaction-use-case/cancel-transaction.use-case'; import { CompleteTransactionUseCase } from './app/complete-transaction-use-case/complete-transaction.use-case'; import { CreateTransactionUseCase } from './app/create-transaction-use-case/create-transaction.use-case'; -import { UpdateTransactionFromWebhookUseCase } from './app/update-transaction-from-webhook-use-case/update-transaction-from-webhook.use-case'; +import { SendCompleteTransactionEventUseCase } from './app/send-complete-trx-event.use-case/send-complete-trx-event.use-case'; +import { UpdateTransactionGatewayResponseUseCase } from './app/update-transaction-gateway-response-use-case/update-transaction-gateway-response.use-case'; import { TransactionService } from './domain/services/transaction.service'; import { TRANSACTION_REPOSITORY_TOKEN } from './domain/transaction.repo'; import { TransactionRepoImpl } from './infra/repo/transaction.postgres.repo.impl'; +import { RabbitController } from './interfaces/rabbit.controller'; +import TransactionMapper from './mappers/transaction.mapper'; @Module({ - imports: [LoggerModule, WalletModule, GatewayModule], - controllers: [TransactionController], + imports: [LoggerModule, WalletModule, GatewayModule, RabbitModule], + controllers: [TransactionController, RabbitController], providers: [ TransactionService, CreateTransactionUseCase, @@ -25,7 +29,9 @@ import { TransactionRepoImpl } from './infra/repo/transaction.postgres.repo.impl provide: TRANSACTION_REPOSITORY_TOKEN, useClass: TransactionRepoImpl, }, - UpdateTransactionFromWebhookUseCase, + UpdateTransactionGatewayResponseUseCase, + SendCompleteTransactionEventUseCase, + TransactionMapper, ], exports: [ TransactionService, @@ -33,7 +39,9 @@ import { TransactionRepoImpl } from './infra/repo/transaction.postgres.repo.impl CompleteTransactionUseCase, TransactionRepresentationMapper, CancelTransactionUseCase, - UpdateTransactionFromWebhookUseCase, + UpdateTransactionGatewayResponseUseCase, + SendCompleteTransactionEventUseCase, + TransactionMapper, ], }) export class TransactionModule {} diff --git a/src/shared/router/routes.ts b/src/shared/router/routes.ts index fe966d2..1d75382 100644 --- a/src/shared/router/routes.ts +++ b/src/shared/router/routes.ts @@ -15,4 +15,5 @@ export enum TransactionRoutes { COMPLETE = 'complete/:walletId', CANCEL = 'cancel/:transactionId', WEBHOOK = 'webhook', + COMPLETE_TRX_EVENTS = 'complete/event/:transactionId', } diff --git a/src/shared/utils/baseClass.ts b/src/shared/utils/baseClass.ts new file mode 100644 index 0000000..3498c3c --- /dev/null +++ b/src/shared/utils/baseClass.ts @@ -0,0 +1,3 @@ +export class BaseClassWithAppLogger { + constructor() {} +} diff --git a/src/shared/validations/transaction/status.ts b/src/shared/validations/transaction/status.ts index adad689..02cc922 100644 --- a/src/shared/validations/transaction/status.ts +++ b/src/shared/validations/transaction/status.ts @@ -3,13 +3,15 @@ export enum TransactionStatusEnum { COMPLETED = 'COMPLETED', FAILED = 'FAILED', CANCELLED = 'CANCELLED', + GATEWAY = 'GATEWAY', } export type TransactionStatusType = | TransactionStatusEnum.CANCELLED | TransactionStatusEnum.COMPLETED | TransactionStatusEnum.FAILED - | TransactionStatusEnum.PENDING; + | TransactionStatusEnum.PENDING + | TransactionStatusEnum.GATEWAY; export const validTransactionStatuses = Object.values(TransactionStatusEnum); @@ -18,4 +20,5 @@ export const TRANSACTION_STATUS_TYPE: TransactionStatusType[] = [ TransactionStatusEnum.COMPLETED, TransactionStatusEnum.FAILED, TransactionStatusEnum.PENDING, + TransactionStatusEnum.GATEWAY, ];