From 4c8a12e704780c67e808979dabff39a2a479274b Mon Sep 17 00:00:00 2001 From: KurtisMelkisedec Date: Fri, 24 Oct 2025 10:10:54 +0000 Subject: [PATCH] webhook to send callback to rabbitMQ --- config/rabbit.config.ts | 8 + package-lock.json | 287 +++++++++++++++++++++++++- package.json | 9 +- src/app.module.ts | 12 +- src/controllers/webhook.controller.ts | 74 +++++++ src/dtos/sms.mo.dto.ts | 39 ++++ src/dtos/subscription.dto.ts | 147 +++++++++++++ src/main.ts | 23 +++ src/services/rabbit.service.ts | 48 +++++ src/services/webhook.service.ts | 60 ++++++ 10 files changed, 698 insertions(+), 9 deletions(-) create mode 100644 config/rabbit.config.ts create mode 100644 src/controllers/webhook.controller.ts create mode 100644 src/dtos/sms.mo.dto.ts create mode 100644 src/dtos/subscription.dto.ts create mode 100644 src/services/rabbit.service.ts create mode 100644 src/services/webhook.service.ts diff --git a/config/rabbit.config.ts b/config/rabbit.config.ts new file mode 100644 index 0000000..f90d624 --- /dev/null +++ b/config/rabbit.config.ts @@ -0,0 +1,8 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('rabbitmq', () => ({ + user: process.env.RABBITMQ_USER, + pass: process.env.RABBITMQ_PASS, + host: process.env.RABBITMQ_HOST, + port: process.env.RABBITMQ_PORT, +})); diff --git a/package-lock.json b/package-lock.json index 90470e3..d6c5b9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,17 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/microservices": "^11.1.7", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.1", + "amqplib": "^0.10.9", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -2053,6 +2060,12 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2330,6 +2343,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/core": { "version": "11.1.7", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.7.tgz", @@ -2372,6 +2400,85 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/microservices": { + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.1.7.tgz", + "integrity": "sha512-Oc+Uqsx5Br0aCZOaQ4n+ykiI3q1nUNZ2zwM6WRxVYG5BWfeicXS0b68abg9LRblLmRJ5pX7NynE86YKNuN20nQ==", + "license": "MIT", + "peer": true, + "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.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz", @@ -2492,6 +2599,39 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/swagger": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.1.tgz", + "integrity": "sha512-1MS7xf0pzc1mofG53xrrtrurnziafPUHkqzRm4YUVPA/egeiMaSerQBD/feiAeQ2BnX0WiLsTX4HQFO0icvOjQ==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.29.4" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.7", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.7.tgz", @@ -2621,6 +2761,13 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -2994,6 +3141,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -3854,6 +4007,20 @@ "ajv": "^6.9.1" } }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "license": "MIT", + "peer": true, + "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", @@ -3963,7 +4130,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -4281,6 +4447,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", @@ -4455,6 +4627,25 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -4924,6 +5115,33 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5393,6 +5611,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -7225,7 +7444,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7342,6 +7560,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.24", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.24.tgz", + "integrity": "sha512-l5IlyL9AONj4voSd7q9xkuQOL4u8Ty44puTic7J88CmdXkxfGsRfoVLXHCxppwehgpb/Chdb80FFehHqjN3ItQ==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7402,7 +7626,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.memoize": { @@ -8366,6 +8589,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", @@ -8483,6 +8712,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", @@ -8593,7 +8828,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -9165,6 +9399,30 @@ "node": ">=8" } }, + "node_modules/swagger-ui-dist": { + "version": "5.29.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.4.tgz", + "integrity": "sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -9894,6 +10152,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", @@ -9922,6 +10190,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 8fa35fa..7ac3757 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,17 @@ }, "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/microservices": "^11.1.7", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.1", + "amqplib": "^0.10.9", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/app.module.ts b/src/app.module.ts index 8662803..a8f8146 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,16 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { WebhookController } from './controllers/webhook.controller'; +import { WebhookService } from './services/webhook.service'; +import { RabbitMQService } from './services/rabbit.service'; +import { ConfigModule } from '@nestjs/config'; @Module({ - imports: [], - controllers: [AppController], - providers: [AppService], + imports: [ + ConfigModule.forRoot({ isGlobal: true,load: [() => require('./config/rabbitmq.config').default()] }), + ], + controllers: [AppController, WebhookController], + providers: [AppService, WebhookService, RabbitMQService], }) export class AppModule {} diff --git a/src/controllers/webhook.controller.ts b/src/controllers/webhook.controller.ts new file mode 100644 index 0000000..2157aeb --- /dev/null +++ b/src/controllers/webhook.controller.ts @@ -0,0 +1,74 @@ +import { + Body, + Controller, + Get, + Param, + Headers, + Post, + Query, + HttpCode, + HttpStatus, +} from '@nestjs/common'; + +import { ApiTags } from '@nestjs/swagger'; +import { InboundSMSMessageNotificationWrapperDto } from 'src/dtos/sms.mo.dto'; +import { SubscriptionDto } from 'src/dtos/subscription.dto'; +import { WebhookService } from 'src/services/webhook.service'; + +@Controller('webhook') +@ApiTags('webhook') +export class WebhookController { + constructor(private readonly webhookService: WebhookService) {} + + @Post('sms-mo/:operator/:country') + @HttpCode(HttpStatus.CREATED) + async smsMoNotification( + @Param('country') country: string, + @Param('operator') operator: string, + @Headers('X-Orange-ISE2') ise2: string, + @Body() dto: InboundSMSMessageNotificationWrapperDto, + ) { + console.log('reception of payload sms-mo notification', dto); + console.log(`country code ${country} operator ${operator}`); + + await this.webhookService.smsMoNotification(country, operator, ise2, dto); + + return { status: 'queued', operator, country }; + } + + @Post('subscription/:operator/:country') + @HttpCode(HttpStatus.CREATED) + async manageSubscription( + @Param('country') country: string, + @Param('operator') operator: string, + @Body() dto: SubscriptionDto, + ) { + console.log('reception of payload sub/unsub user', dto); + console.log(`country code ${country} operator ${operator}`); + + await this.webhookService.manageSubscription(country, operator, dto); + + return { status: 'queued', operator, country }; + } + + @Get('he/:operator/:country') + @HttpCode(HttpStatus.OK) + async heNotification( + @Param('country') country: string, + @Param('operator') operator: string, + @Query('callback') callback: string, + @Headers('X-WASSUP-ISE2') ise: string, + ) { + console.log('he notification get with callback', callback); + console.log(`country code ${country} operator ${operator}`); + + await this.webhookService.handleHeNotification( + country, + operator, + ise, + callback, + ); + + return { status: 'queued', operator, country, callback }; + } +} diff --git a/src/dtos/sms.mo.dto.ts b/src/dtos/sms.mo.dto.ts new file mode 100644 index 0000000..679bbb6 --- /dev/null +++ b/src/dtos/sms.mo.dto.ts @@ -0,0 +1,39 @@ +import { IsString, ValidateNested, IsNotEmpty, IsDateString } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class InboundSMSMessageDto { + @IsDateString() + dateTime: string; + + @IsString() + @IsNotEmpty() + destinationAddress: string; + + @IsString() + @IsNotEmpty() + messageId: string; + + @IsString() + @IsNotEmpty() + message: string; + + @IsString() + @IsNotEmpty() + senderAddress: string; +} + +export class InboundSMSMessageNotificationDto { + @IsString() + @IsNotEmpty() + callbackData: string; + + @ValidateNested() + @Type(() => InboundSMSMessageDto) + inboundSMSMessage: InboundSMSMessageDto; +} + +export class InboundSMSMessageNotificationWrapperDto { + @ValidateNested() + @Type(() => InboundSMSMessageNotificationDto) + inboundSMSMessageNotification: InboundSMSMessageNotificationDto; +} diff --git a/src/dtos/subscription.dto.ts b/src/dtos/subscription.dto.ts new file mode 100644 index 0000000..7428520 --- /dev/null +++ b/src/dtos/subscription.dto.ts @@ -0,0 +1,147 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsString, IsNumber, IsOptional, ValidateNested, IsArray, IsEnum } from 'class-validator'; + +class NoteDto { + @ApiProperty({ example: "partner data" }) + @IsString() + text: string; +} + +class RelatedPartyDto { + @ApiProperty({ example: "PDKSUB-200-..." }) + @IsString() + id: string; + + @ApiProperty({ example: "ISE2" }) + @IsString() + name: string; + + @ApiProperty({ example: "subscriber" }) + @IsString() + role: string; +} + +class ProductCharacteristicDto { + @ApiProperty({ example: "periodicity" }) + @IsString() + name: string; + + @ApiProperty({ example: "7" }) + @IsString() + value: string; +} + +class ProductDto { + @ApiProperty({ example: "WIDO access" }) + @IsString() + id: string; + + @ApiProperty({ example: "http://www.digster.eg/monthly" }) + @IsString() + @IsOptional() + href?: string; + + @ApiProperty({ type: [ProductCharacteristicDto] }) + @ValidateNested({ each: true }) + @Type(() => ProductCharacteristicDto) + @IsOptional() + productCharacteristic?: ProductCharacteristicDto[]; +} + +class OrderItemDto { + @ApiProperty({ example: 2.99 }) + @IsNumber() + @IsOptional() + chargedAmount?: number; + + @ApiProperty({ example: "GNF" }) + @IsString() + @IsOptional() + currency?: string; + + @ApiProperty({ example: "2017-06-15T16:00:00-04:00Z" }) + @IsString() + @IsOptional() + validityDate?: string; + + @ApiProperty({ example: "2017-06-15T16:00:00-04:00Z" }) + @IsString() + @IsOptional() + nextCharge?: string; + + @ApiProperty({ type: ProductDto }) + @ValidateNested() + @Type(() => ProductDto) + product: ProductDto; +} + +export enum OrderState { + Completed = 'Completed', + Pending = 'Failed', +} +export enum EventType { + creation = 'orderCreation', + deletion = 'orderDeletion', +} + +class OrderDto { + @ApiProperty({ example: 21345 }) + @IsNumber() + id: number; + + @ApiProperty({ + example: 'Completed', + enum: OrderState, + description: 'order state (Completed or Failed)' + }) + @IsEnum(OrderState) + state: OrderState; + + @ApiProperty({ type: OrderItemDto }) + @ValidateNested() + @Type(() => OrderItemDto) + orderItem: OrderItemDto; +} + +class EventDto { + @ApiProperty({ example: 465487 }) + @IsNumber() + id: number; + + @ApiProperty({ type: [RelatedPartyDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => RelatedPartyDto) + relatedParty: RelatedPartyDto[]; + + @ApiProperty({ type: OrderDto }) + @ValidateNested() + @Type(() => OrderDto) + order: OrderDto; +} + +export class SubscriptionDto { + @ApiProperty({ type: NoteDto }) + @ValidateNested() + @Type(() => NoteDto) + note: NoteDto; + + @ApiProperty({ type: EventDto }) + @ValidateNested() + @Type(() => EventDto) + event: EventDto; + + + @ApiProperty({ + example: 'orderCreation', + enum: OrderState, + description: 'event type, orderCreation or orderDeletion' + }) + @IsEnum(EventType) + eventType: EventType; + + @ApiProperty({ example: "2017-06-12T16:00:00-04:00Z" }) + @IsString() + eventTime: string; +} diff --git a/src/main.ts b/src/main.ts index f76bc8d..04ed2bd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,31 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { ValidationPipe } from '@nestjs/common/pipes/validation.pipe'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + + app.setGlobalPrefix('api/v1'); + //dto validation + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + + //swagger configuration + + const config = new DocumentBuilder() + .setTitle('DCB webhook service') + .setDescription( + 'This is a service dedicated to the reception of callback from external source and sending to rabbitMQ', + ) + .setVersion('1.0') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document); + await app.listen(process.env.PORT ?? 3000); + console.log(`Application is running on: http://localhost:3000`); + console.log(`Swagger docs: http://localhost:3000/api/docs`); } bootstrap(); diff --git a/src/services/rabbit.service.ts b/src/services/rabbit.service.ts new file mode 100644 index 0000000..f6853c5 --- /dev/null +++ b/src/services/rabbit.service.ts @@ -0,0 +1,48 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { connect, Connection, Channel } from 'amqplib'; + +@Injectable() +export class RabbitMQService implements OnModuleInit { + private connection!: Connection; + private channel!: Channel; + + constructor(private configService: ConfigService) {} + + async onModuleInit() { + await this.connectWithRetry(); + } + + async connect(): Promise { + const user = this.configService.get('rabbitmq.user'); + const pass = this.configService.get('rabbitmq.pass'); + const host = this.configService.get('rabbitmq.host'); + const port = this.configService.get('rabbitmq.port'); + + this.connection = await connect(`amqp://${user}:${pass}@${host}:${port}`); + this.channel = await this.connection.createChannel(); + console.log('Connected to RabbitMQ'); + } + + async connectWithRetry(retries = 5, delayMs = 3000): Promise { + for (let i = 0; i < retries; i++) { + try { + await this.connect(); + return; + } catch (err) { + console.error( + `RabbitMQ connection failed, retrying in ${delayMs}ms... (${i + 1}/${retries})`, + ); + await new Promise((res) => setTimeout(res, delayMs)); + } + } + throw new Error('Could not connect to RabbitMQ after multiple attempts'); + } + + async sendToQueue(queue: string, message: any) { + if (!this.channel) throw new Error('RabbitMQ channel not initialized'); + await this.channel.assertQueue(queue, { durable: true }); + this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message))); + console.log(`Sent message to queue "${queue}"`); + } +} diff --git a/src/services/webhook.service.ts b/src/services/webhook.service.ts new file mode 100644 index 0000000..3ddf65a --- /dev/null +++ b/src/services/webhook.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { RabbitMQService } from 'src/services/rabbit.service'; +import { InboundSMSMessageNotificationWrapperDto } from '../dtos/sms.mo.dto'; +import { SubscriptionDto } from '../dtos/subscription.dto'; + +@Injectable() +export class WebhookService { + constructor(private readonly rabbitMQService: RabbitMQService) {} + + async smsMoNotification( + country: string, + operator: string, + ise2: string, + dto: InboundSMSMessageNotificationWrapperDto, + ) { + const payload = { + operator, + country, + ise2, + data: dto, + receivedAt: new Date().toISOString(), + }; + + await this.rabbitMQService.sendToQueue('sms_mo', payload); + } + + async manageSubscription( + country: string, + operator: string, + dto: SubscriptionDto, + ) { + const payload = { + operator, + country, + data: dto, + receivedAt: new Date().toISOString(), + }; + + // send message to queue "subscription_events" + await this.rabbitMQService.sendToQueue('subscription_events', payload); + console.log('payload sent to rabbitMQ'); + } + + async handleHeNotification( + country: string, + operator: string, + callback: string, + ise2: string, + ) { + const payload = { + operator, + country, + ise2, + callback, + receivedAt: new Date().toISOString(), + }; + await this.rabbitMQService.sendToQueue('he_notifications', payload); + console.log('payload sent to rabbitMQ'); + } +}