diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..53f9963 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + # Redis - Required for BullMQ background jobs (notifications, SLAs) + redis: + image: redis:7.2-alpine + container_name: re-onboarding-redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --save 60 1 --loglevel warning + + # RedisInsight - GUI for monitoring Redis/BullMQ (Optional) + redis-insight: + image: redislabs/redisinsight:latest + container_name: re-onboarding-redis-insight + restart: always + ports: + - "8001:8001" + depends_on: + - redis + + # PostgreSQL - Application Database + postgres: + image: postgres:15-alpine + container_name: re-onboarding-db + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: Admin@123 + POSTGRES_DB: royal_enfield_onboarding + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + redis_data: + postgres_data: diff --git a/package-lock.json b/package-lock.json index cf38248..8a50489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "PROPRIETARY", "dependencies": { "bcryptjs": "^3.0.3", + "bullmq": "^5.73.4", "compression": "^1.8.1", "cors": "^2.8.5", "dotenv": "^17.2.3", @@ -18,6 +19,7 @@ "express-validator": "^7.3.1", "handlebars": "^4.7.8", "helmet": "^8.1.0", + "ioredis": "^5.10.1", "jsonwebtoken": "^9.0.3", "multer": "^2.0.2", "nodemailer": "^7.0.12", @@ -1867,6 +1869,12 @@ "node": ">=18" } }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2304,6 +2312,84 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "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", @@ -4212,6 +4298,46 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bullmq": { + "version": "5.73.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.73.4.tgz", + "integrity": "sha512-Q+NeFLtdKSD3GDPYSX4pH+Mc9E4OZVKimXwrnZ5WmndNy31COMy4vQV9zfhgfHGSUFrlpsBicfKYbSjx9FbO+A==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.10.1", + "msgpackr": "1.11.5", + "node-abort-controller": "3.1.1", + "semver": "7.7.4", + "tslib": "2.8.1", + "uuid": "11.1.0" + } + }, + "node_modules/bullmq/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/bullmq/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4455,6 +4581,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4697,6 +4832,18 @@ "dev": true, "license": "MIT" }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4756,6 +4903,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4765,6 +4921,16 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5949,6 +6115,53 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/ip-address": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", @@ -6953,12 +7166,24 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -7028,6 +7253,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -7265,6 +7499,37 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/multer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", @@ -7321,6 +7586,27 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8008,6 +8294,27 @@ "node": ">=8.10.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8680,6 +8987,12 @@ "node": ">=10" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -9134,7 +9447,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tsx": { diff --git a/package.json b/package.json index 3e01ad8..5967ff9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "license": "PROPRIETARY", "dependencies": { "bcryptjs": "^3.0.3", + "bullmq": "^5.73.4", "compression": "^1.8.1", "cors": "^2.8.5", "dotenv": "^17.2.3", @@ -44,6 +45,7 @@ "express-validator": "^7.3.1", "handlebars": "^4.7.8", "helmet": "^8.1.0", + "ioredis": "^5.10.1", "jsonwebtoken": "^9.0.3", "multer": "^2.0.2", "nodemailer": "^7.0.12", @@ -81,4 +83,4 @@ "node": ">=18.0.0", "npm": ">=9.0.0" } -} \ No newline at end of file +} diff --git a/scratch/check_fnf.sql b/scratch/check_fnf.sql new file mode 100644 index 0000000..690fd73 --- /dev/null +++ b/scratch/check_fnf.sql @@ -0,0 +1 @@ +SELECT id, "settlementId", "outletId", "dealerId", status FROM fnf_settlements ORDER BY "createdAt" DESC LIMIT 1; diff --git a/scratch/fix_schema.sql b/scratch/fix_schema.sql new file mode 100644 index 0000000..cabe0e9 --- /dev/null +++ b/scratch/fix_schema.sql @@ -0,0 +1,3 @@ +ALTER TABLE fnf_settlements DROP COLUMN IF EXISTS settlementid; +ALTER TABLE fnf_settlements DROP COLUMN IF EXISTS "settlementId"; +ALTER TABLE fnf_settlements ADD COLUMN "settlementId" VARCHAR(255); diff --git a/scripts/add-recovery-enum.ts b/scripts/add-recovery-enum.ts new file mode 100644 index 0000000..2f3b540 --- /dev/null +++ b/scripts/add-recovery-enum.ts @@ -0,0 +1,27 @@ +import 'dotenv/config'; +import db from '../src/database/models/index.js'; + +const addRecoveryEnum = async () => { + try { + console.log('>>> STARTING ENUM UPDATE: FnFLineItem itemType <<<'); + await db.sequelize.authenticate(); + console.log('Database connection established.'); + + // Raw query to add 'Recovery' to the itemType enum + try { + await db.sequelize.query(`ALTER TYPE "enum_fnf_line_items_itemType" ADD VALUE IF NOT EXISTS 'Recovery';`); + console.log('SUCCESS: Added "Recovery" to "enum_fnf_line_items_itemType"'); + } catch (e) { + console.log('NOTICE: "Recovery" likely already exists or another error occurred:', e.message); + } + + await db.sequelize.close(); + console.log('>>> ENUM UPDATE COMPLETED <<<'); + process.exit(0); + } catch (error) { + console.error('>>> ERROR: Failed to update Enum:', error); + process.exit(1); + } +}; + +addRecoveryEnum(); diff --git a/simulate-clearances.js b/simulate-clearances.js new file mode 100644 index 0000000..438f78d --- /dev/null +++ b/simulate-clearances.js @@ -0,0 +1,102 @@ +const BASE_URL = 'http://localhost:5000/api'; +const FINANCE_EMAIL = 'finance@royalenfield.com'; +const PASSWORD = 'Admin@123'; + +async function simulateFullClearance() { + try { + console.log('--- STARTING COMPREHENSIVE F&F SIMULATION (16 DEPARTMENTS) ---'); + + // 1. Login + const loginRes = await fetch(`${BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: FINANCE_EMAIL, password: PASSWORD }) + }); + const loginData = await loginRes.json(); + const token = loginData.token; + const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }; + + // 2. Resolve target F&F + const res = await fetch(`${BASE_URL}/settlement/fnf`, { headers }); + const data = await res.json(); + const latestFnF = data.settlements[0]; + + if (!latestFnF) { + console.error('No F&F record found.'); + return; + } + const fnfId = latestFnF.id; + console.log(`Targeting F&F: ${fnfId} for ${latestFnF.outlet?.name}`); + + // 3. Define departmental scenarios + const scenarios = [ + { dept: 'Sales', type: 'Receivable', amount: 45000, desc: 'Pending vehicle stock dues' }, + { dept: 'Service', type: 'Payable', amount: 12000, desc: 'Unprocessed warranty claims' }, + { dept: 'Spares / Parts', type: 'Receivable', amount: 8500, desc: 'Missing spare parts inventory' }, + { dept: 'Warranty', type: 'Payable', amount: 3400, desc: 'Settled warranty credit' }, + { dept: 'Accounts', type: 'Receivable', amount: 1500, desc: 'Audit penalty' }, + { dept: 'Marketing', type: 'Payable', amount: 20000, desc: 'Signage reimbursement' } + ]; + + // 4. Add Line Items + console.log('\nAdding financial line items...'); + for (const item of scenarios) { + await fetch(`${BASE_URL}/settlement/fnf/${fnfId}/line-items`, { + method: 'POST', + headers, + body: JSON.stringify({ + itemType: item.type, + description: item.desc, + department: item.dept, + amount: item.amount + }) + }); + console.log(`[+] Added ${item.type} for ${item.dept}: ₹${item.amount}`); + } + + // 5. Submit Clearances for ALL 16 Departments + console.log('\nSubmitting clearances for all 16 departments...'); + const allDepts = latestFnF.clearances; + + for (const clearance of allDepts) { + const hasDues = scenarios.some(s => s.dept === clearance.department); + + await fetch(`${BASE_URL}/settlement/fnf/${fnfId}/clearances/${clearance.id}`, { + method: 'PUT', + headers, + body: JSON.stringify({ + status: 'Cleared', // Backend will auto-convert to 'NOC Submitted' or 'Dues Pending' + remarks: hasDues + ? `Clearance submitted with outstanding dues as listed in calculations.` + : `No dues found. Department NOC issued successfully.`, + supportingDocument: `https://re-dealer-onboard.s3.amazonaws.com/noc/${clearance.department.replace(/\s/g, '_')}_NOC.pdf` + }) + }); + console.log(`[OK] Cleared ${clearance.department} (${hasDues ? 'With Dues' : 'Full NOC'})`); + } + + // 6. Verify Final State + const finalRes = await fetch(`${BASE_URL}/settlement/fnf/${fnfId}`, { headers }); + const finalData = await finalRes.json(); + const f = finalData.fnf; + + console.log('\n--- FINAL VERIFICATION ---'); + console.log(`Case Number: ${f.resignation?.resignationId || f.id}`); + console.log(`Overall Status: ${f.status}`); + console.log(`Overall Progress: ${f.progressPercentage}%`); + console.log(`Net Settlement Amount: ₹${f.netAmount}`); + console.log(`Departments with Dues: ${f.clearances.filter(c => c.status === 'Dues Pending').length}`); + console.log(`Departments with NOC: ${f.clearances.filter(c => c.status === 'NOC Submitted').length}`); + + console.log('\nUI CHECKLIST:'); + console.log('1. Verify Step 2 of progress bar is Green (Completed).'); + console.log('2. Verify Step 3 (Finance Summary) is Blue (In Progress).'); + console.log('3. Verify Status Badges in table: 6 Red (Dues Pending), 10 Green (NOC Submitted).'); + console.log('4. Verify Net Amount matches the sum of added items.'); + + } catch (error) { + console.error('Simulation Failed:', error); + } +} + +simulateFullClearance(); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 3c9e1d7..e8bf195 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -20,7 +20,16 @@ export const ROLES = { CEO: 'CEO', SPARES_MANAGER: 'Spares Manager', SERVICE_MANAGER: 'Service Manager', - ACCOUNTS_MANAGER: 'Accounts Manager' + ACCOUNTS_MANAGER: 'Accounts Manager', + SALES_MANAGER: 'Sales Manager', + WARRANTY_MANAGER: 'Warranty Manager', + MARKETING_MANAGER: 'Marketing Manager', + HR_MANAGER: 'HR Manager', + IT_MANAGER: 'IT Manager', + LOGISTICS_MANAGER: 'Logistics Manager', + QUALITY_MANAGER: 'Quality Manager', + APPAREL_MANAGER: 'Apparel Manager', + DMS_MANAGER: 'DMS Manager' } as const; // Regions @@ -115,6 +124,7 @@ export const TERMINATION_STAGES = { ZBH_REVIEW: 'ZBH Review', DD_LEAD_REVIEW: 'DD Lead Review', LEGAL_VERIFICATION: 'Legal Verification', + DD_HEAD_REVIEW: 'DD Head Review', NBH_EVALUATION: 'NBH Evaluation', SCN_ISSUED: 'Show Cause Notice', PERSONAL_HEARING: 'Personal Hearing', @@ -246,24 +256,24 @@ export const FNF_STATUS = { COMPLETED: 'Completed' } as const; -// F&F Departments (Full list of 16 functional units) +// F&F Departments (Full list of 16 functional units as per Finance Dashboard) export const FNF_DEPARTMENTS = [ - 'Sales', - 'Service', - 'Spares / Parts', - 'Finance', - 'Accounts', - 'Warranty', - 'Marketing', - 'HR', - 'IT', - 'Legal', - 'Logistics', - 'Quality', - 'FDD', - 'Apparel', - 'DMS', - 'Admin / DD-Admin' + 'Warranty Department', + 'Accessories Department', + 'Sales Department', + 'RTO Department', + 'Service Department', + 'Parts Department', + 'Finance Department', + 'Insurance Department', + 'Inventory Department', + 'Marketing Department', + 'HR Department', + 'IT Department', + 'Legal Department', + 'Quality Department', + 'Logistics Department', + 'Customer Relations Department' ]; // Audit Actions diff --git a/src/common/queues/config.ts b/src/common/queues/config.ts new file mode 100644 index 0000000..50bcfe2 --- /dev/null +++ b/src/common/queues/config.ts @@ -0,0 +1,9 @@ +import { ConnectionOptions } from 'bullmq'; +import dotenv from 'dotenv'; +dotenv.config(); + +export const redisConfig: ConnectionOptions = { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379'), + password: process.env.REDIS_PASSWORD || undefined, +}; diff --git a/src/common/queues/notification.queue.ts b/src/common/queues/notification.queue.ts new file mode 100644 index 0000000..cae66fb --- /dev/null +++ b/src/common/queues/notification.queue.ts @@ -0,0 +1,16 @@ +import { Queue } from 'bullmq'; +import { redisConfig } from './config.js'; + +export const notificationQueue = new Queue('notificationQueue', { + connection: redisConfig +}); + +export const addNotificationJob = async (data: any) => { + await notificationQueue.add('sendNotification', data, { + attempts: 3, + backoff: { + type: 'exponential', + delay: 5000 + } + }); +}; diff --git a/src/common/queues/notification.worker.ts b/src/common/queues/notification.worker.ts new file mode 100644 index 0000000..49a960f --- /dev/null +++ b/src/common/queues/notification.worker.ts @@ -0,0 +1,22 @@ +import { Worker } from 'bullmq'; +import { redisConfig } from './config.js'; +import { NotificationService } from '../../services/NotificationService.js'; + +export const notificationWorker = new Worker('notificationQueue', async (job) => { + console.log(`[Worker] Processing job ${job.id} of type ${job.name}`); + + if (job.name === 'sendNotification') { + await NotificationService.processJob(job.data); + } +}, { + connection: redisConfig, + concurrency: 5 +}); + +notificationWorker.on('completed', (job) => { + console.log(`[Worker] Job ${job.id} completed successfully`); +}); + +notificationWorker.on('failed', (job, err) => { + console.error(`[Worker] Job ${job?.id} failed with error: ${err.message}`); +}); diff --git a/src/common/queues/sla.queue.ts b/src/common/queues/sla.queue.ts new file mode 100644 index 0000000..750dbbe --- /dev/null +++ b/src/common/queues/sla.queue.ts @@ -0,0 +1,20 @@ +import { Queue } from 'bullmq'; +import { redisConfig } from './config.js'; + +export const slaQueue = new Queue('slaQueue', { + connection: redisConfig +}); + +/** + * Schedule the recurring SLA check (Every Hour) + */ +export const scheduleSLACheck = async () => { + // We use a unique job ID to ensure only one instance of the repeatable job exists + await slaQueue.add('checkSLABreaches', {}, { + repeat: { + pattern: '0 * * * *', // Every hour at :00 + }, + jobId: 'hourly-sla-check' + }); + console.log('[SLA Queue] Repeatable job scheduled: Hourly check'); +}; diff --git a/src/common/queues/sla.worker.ts b/src/common/queues/sla.worker.ts new file mode 100644 index 0000000..6dd0eab --- /dev/null +++ b/src/common/queues/sla.worker.ts @@ -0,0 +1,22 @@ +import { Worker } from 'bullmq'; +import { redisConfig } from './config.js'; +import { SLAService } from '../../services/SLAService.js'; + +export const slaWorker = new Worker('slaQueue', async (job) => { + console.log(`[SLA Worker] Processing job ${job.id} of type ${job.name}`); + + if (job.name === 'checkSLABreaches') { + await SLAService.checkBreaches(); + } +}, { + connection: redisConfig, + concurrency: 1 // Single concurrency for SLA checks to avoid race conditions +}); + +slaWorker.on('completed', (job) => { + console.log(`[SLA Worker] Job ${job.id} completed successfully`); +}); + +slaWorker.on('failed', (job, err) => { + console.error(`[SLA Worker] Job ${job?.id} failed with error: ${err.message}`); +}); diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index ef833d5..3b0c839 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -40,6 +40,15 @@ export interface ApplicationAttributes { architectureDocumentDate: Date | null; architectureCompletionDate: Date | null; score: number; + // Statutory & Bank Details + panNumber: string | null; + gstNumber: string | null; + bankName: string | null; + accountNumber: string | null; + ifscCode: string | null; + branchName: string | null; + accountHolderName: string | null; + registeredAddress: string | null; documents: any[]; timeline: any[]; } @@ -198,6 +207,10 @@ export default (sequelize: Sequelize) => { allowNull: true, defaultValue: 'Pending' }, + registeredAddress: { + type: DataTypes.TEXT, + allowNull: true + }, submittedBy: { type: DataTypes.UUID, allowNull: true, @@ -226,6 +239,34 @@ export default (sequelize: Sequelize) => { type: DataTypes.DATE, allowNull: true }, + panNumber: { + type: DataTypes.STRING, + allowNull: true + }, + gstNumber: { + type: DataTypes.STRING, + allowNull: true + }, + bankName: { + type: DataTypes.STRING, + allowNull: true + }, + accountNumber: { + type: DataTypes.STRING, + allowNull: true + }, + ifscCode: { + type: DataTypes.STRING, + allowNull: true + }, + branchName: { + type: DataTypes.STRING, + allowNull: true + }, + accountHolderName: { + type: DataTypes.STRING, + allowNull: true + }, documents: { type: DataTypes.JSON, defaultValue: [] diff --git a/src/database/models/ConstitutionalChange.ts b/src/database/models/ConstitutionalChange.ts index 852b3bf..8e05041 100644 --- a/src/database/models/ConstitutionalChange.ts +++ b/src/database/models/ConstitutionalChange.ts @@ -126,7 +126,8 @@ export default (sequelize: Sequelize) => { ConstitutionalChange.hasMany(models.RequestParticipant, { foreignKey: 'requestId', as: 'participants', - scope: { requestType: 'constitutional' } + scope: { requestType: 'constitutional' }, + constraints: false }); }; diff --git a/src/database/models/Dealer.ts b/src/database/models/Dealer.ts index 67d8359..e2e2090 100644 --- a/src/database/models/Dealer.ts +++ b/src/database/models/Dealer.ts @@ -132,6 +132,8 @@ export default (sequelize: Sequelize) => { if (User) { Dealer.hasOne(User, { foreignKey: 'dealerId', as: 'user' }); } + + Dealer.hasMany(models.DealerBankDetail, { foreignKey: 'dealerId', as: 'bankDetails' }); }; return Dealer; diff --git a/src/database/models/DealerBankDetail.ts b/src/database/models/DealerBankDetail.ts new file mode 100644 index 0000000..b7519c6 --- /dev/null +++ b/src/database/models/DealerBankDetail.ts @@ -0,0 +1,74 @@ +import { Model, DataTypes, Sequelize } from 'sequelize'; + +export interface DealerBankDetailAttributes { + id: string; + dealerId: string; + accountHolderName: string; + accountNumber: string; + ifscCode: string; + bankName: string; + branchName: string; + isPrimary: boolean; + status: string; +} + +export interface DealerBankDetailInstance extends Model, DealerBankDetailAttributes { } + +export default (sequelize: Sequelize) => { + const DealerBankDetail = sequelize.define('DealerBankDetail', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + dealerId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'dealers', + key: 'id' + } + }, + accountHolderName: { + type: DataTypes.STRING, + allowNull: false + }, + accountNumber: { + type: DataTypes.STRING, + allowNull: false + }, + ifscCode: { + type: DataTypes.STRING, + allowNull: false + }, + bankName: { + type: DataTypes.STRING, + allowNull: false + }, + branchName: { + type: DataTypes.STRING, + allowNull: true + }, + isPrimary: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + } + }, { + tableName: 'dealer_bank_details', + timestamps: true, + indexes: [ + { fields: ['dealerId'] }, + { fields: ['status'] } + ] + }); + + (DealerBankDetail as any).associate = (models: any) => { + DealerBankDetail.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealer' }); + }; + + return DealerBankDetail; +}; diff --git a/src/database/models/FnF.ts b/src/database/models/FnF.ts index fb38362..39fa13d 100644 --- a/src/database/models/FnF.ts +++ b/src/database/models/FnF.ts @@ -3,6 +3,7 @@ import { FNF_STATUS } from '../../common/config/constants.js'; export interface FnFAttributes { id: string; + settlementId: string | null; resignationId: string | null; terminationRequestId: string | null; outletId: string | null; @@ -10,8 +11,13 @@ export interface FnFAttributes { status: string; totalReceivables: number; totalPayables: number; + totalDeductions: number; netAmount: number; + settlementAmount: number | null; settlementDate: Date | null; + paymentMode: string | null; + transactionReference: string | null; + remarks: string | null; clearanceDocuments: any[]; progressPercentage: number; } @@ -25,6 +31,11 @@ export default (sequelize: Sequelize) => { defaultValue: DataTypes.UUIDV4, primaryKey: true }, + settlementId: { + type: DataTypes.STRING, + allowNull: true, + unique: true + }, resignationId: { type: DataTypes.UUID, allowNull: true, @@ -69,14 +80,34 @@ export default (sequelize: Sequelize) => { type: DataTypes.DECIMAL(15, 2), defaultValue: 0 }, + totalDeductions: { + type: DataTypes.DECIMAL(15, 2), + defaultValue: 0 + }, netAmount: { type: DataTypes.DECIMAL(15, 2), defaultValue: 0 }, + settlementAmount: { + type: DataTypes.DECIMAL(15, 2), + allowNull: true + }, settlementDate: { type: DataTypes.DATE, allowNull: true }, + paymentMode: { + type: DataTypes.STRING, + allowNull: true + }, + transactionReference: { + type: DataTypes.STRING, + allowNull: true + }, + remarks: { + type: DataTypes.TEXT, + allowNull: true + }, clearanceDocuments: { type: DataTypes.JSON, defaultValue: [] diff --git a/src/database/models/FnFLineItem.ts b/src/database/models/FnFLineItem.ts index 3cf9791..55789b8 100644 --- a/src/database/models/FnFLineItem.ts +++ b/src/database/models/FnFLineItem.ts @@ -3,7 +3,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize'; export interface FnFLineItemAttributes { id: string; fnfId: string; - itemType: 'Payable' | 'Receivable' | 'Deduction'; + itemType: 'Payable' | 'Receivable' | 'Deduction' | 'Recovery'; description: string; department: string; amount: number; @@ -28,7 +28,7 @@ export default (sequelize: Sequelize) => { } }, itemType: { - type: DataTypes.ENUM('Payable', 'Receivable', 'Deduction'), + type: DataTypes.ENUM('Payable', 'Receivable', 'Deduction', 'Recovery'), allowNull: false }, description: { diff --git a/src/database/models/RelocationRequest.ts b/src/database/models/RelocationRequest.ts index 365f770..181b057 100644 --- a/src/database/models/RelocationRequest.ts +++ b/src/database/models/RelocationRequest.ts @@ -148,6 +148,12 @@ export default (sequelize: Sequelize) => { foreignKey: 'relocationId', as: 'eorChecklist' }); + RelocationRequest.hasMany(models.RequestParticipant, { + foreignKey: 'requestId', + as: 'participants', + scope: { requestType: 'relocation' }, + constraints: false + }); // Note: Participants are computed dynamically based on outlet location hierarchy // See getRequestById in relocation.controller.ts }; diff --git a/src/database/models/TerminationRequest.ts b/src/database/models/TerminationRequest.ts index 45d7275..1412552 100644 --- a/src/database/models/TerminationRequest.ts +++ b/src/database/models/TerminationRequest.ts @@ -13,12 +13,14 @@ export interface TerminationRequestAttributes { comments: string | null; timeline: any[]; documents: any[]; - departmentalClearances: { - spares: boolean; - service: boolean; - accounts: boolean; - logistics: boolean; - } | null; + departmentalClearances: Record | null; } export interface TerminationRequestInstance extends Model, TerminationRequestAttributes { } @@ -85,12 +87,7 @@ export default (sequelize: Sequelize) => { }, departmentalClearances: { type: DataTypes.JSON, - defaultValue: { - spares: false, - service: false, - accounts: false, - logistics: false - } + defaultValue: {} } }, { tableName: 'termination_requests', diff --git a/src/database/models/User.ts b/src/database/models/User.ts index f8a8209..490ac0d 100644 --- a/src/database/models/User.ts +++ b/src/database/models/User.ts @@ -146,6 +146,7 @@ export default (sequelize: Sequelize) => { User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' }); User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' }); + User.hasMany(models.Outlet, { foreignKey: 'dealerId', as: 'outlets' }); }; return User; diff --git a/src/database/models/index.ts b/src/database/models/index.ts index ba98659..f910b92 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -33,6 +33,7 @@ import createState from './State.js'; import createTerminationScnResponse from './TerminationScnResponse.js'; import createTerminationHearingRecord from './TerminationHearingRecord.js'; import createFffClearance from './FffClearance.js'; +import createDealerBankDetail from './DealerBankDetail.js'; // Batch 1: Organizational Hierarchy & User Management import createRole from './Role.js'; @@ -146,6 +147,7 @@ db.State = createState(sequelize); db.TerminationScnResponse = createTerminationScnResponse(sequelize); db.TerminationHearingRecord = createTerminationHearingRecord(sequelize); db.FffClearance = createFffClearance(sequelize); +db.DealerBankDetail = createDealerBankDetail(sequelize); // Batch 1: Organizational Hierarchy & User Management db.Role = createRole(sequelize); diff --git a/src/emailtemplates/applicant_shortlisted.html b/src/emailtemplates/applicant_shortlisted.html index 3ddb4f8..488f819 100644 --- a/src/emailtemplates/applicant_shortlisted.html +++ b/src/emailtemplates/applicant_shortlisted.html @@ -1,47 +1,34 @@ - + - - - Great News: You Are Shortlisted!
-

Royal Enfield

-

Congratulations!

+

ROYAL ENFIELD

-

Dear {{applicantName}},

-

We are excited to inform you that your application ({{applicationId}}) for the Royal Enfield dealership in {{location}} has been SHORTLISTED by our Dealer Development team.

- -
-

You have successfully cleared the initial assessment phase. The next step will be an Initial Evaluation Interview with our Zonal and Regional managers.

- Log In to Onboarding Portal +

Hi {{applicantName}},

+

We are pleased to inform you that your dealership application for {{location}} has been shortlisted for further evaluation.

+

Our team will contact you shortly to schedule the next level of interviews. In the meantime, you can track your application status on our dealer portal.

+ - -

What happens next?

-
    -
  • Our team will schedule an interview slot for you soon.
  • -
  • You will receive a separate notification with the meeting details (Online/Offline).
  • -
  • Please ensure all your business documents are ready for the evaluation.
  • -
- -

We look forward to meeting you and discussing this opportunity further.

- -

Best regards,
Dealer Development Team
Royal Enfield

+

Regards,
Royal Enfield Dealer Development Team

diff --git a/src/emailtemplates/constitutional_change_submitted.html b/src/emailtemplates/constitutional_change_submitted.html new file mode 100644 index 0000000..12eb73e --- /dev/null +++ b/src/emailtemplates/constitutional_change_submitted.html @@ -0,0 +1,30 @@ + + + + + + +
+

ROYAL ENFIELD

+
+

Hi Team,

+

A new Constitutional Change request has been submitted by {{dealerName}}.

+

Request Type: {{changeType}}
Request ID: {{requestId}}

+

Please log in to the Dealer development portal to review the request and documents.

+ +
+ +
+ + diff --git a/src/emailtemplates/constitutional_change_update.html b/src/emailtemplates/constitutional_change_update.html new file mode 100644 index 0000000..e5052b1 --- /dev/null +++ b/src/emailtemplates/constitutional_change_update.html @@ -0,0 +1,30 @@ + + + + + + +
+

ROYAL ENFIELD

+
+

Hi {{dealerName}},

+

This is to inform you that your Constitutional Change request has been updated.

+

Current Stage: {{status}}

+

{{remarks}}

+ +
+ +
+ + diff --git a/src/emailtemplates/dealer_code_ready.html b/src/emailtemplates/dealer_code_ready.html new file mode 100644 index 0000000..9601be9 --- /dev/null +++ b/src/emailtemplates/dealer_code_ready.html @@ -0,0 +1,31 @@ + + + + + + +
+

ROYAL ENFIELD

+
+

SAP Dealer Codes Generated

+

Hi {{applicantName}},

+

We are pleased to inform you that your SAP Dealer Codes for application **{{applicationId}}** are now ready and active in our system.

+
+ Sales Code: {{salesCode}}
+ Service Code: {{serviceCode}} +
+

You can now proceed with system onboarding and initial orders.

+
+ +
+ + diff --git a/src/emailtemplates/generic_notification.html b/src/emailtemplates/generic_notification.html new file mode 100644 index 0000000..f877ce4 --- /dev/null +++ b/src/emailtemplates/generic_notification.html @@ -0,0 +1,25 @@ + + + + + + +
+

ROYAL ENFIELD

+
+

{{title}}

+

{{message}}

+

Please log in to the portal for more details.

+
+ +
+ + diff --git a/src/emailtemplates/interview_scheduled.html b/src/emailtemplates/interview_scheduled.html index 3851d3b..e89ef44 100644 --- a/src/emailtemplates/interview_scheduled.html +++ b/src/emailtemplates/interview_scheduled.html @@ -2,27 +2,30 @@
-
Interview Scheduled: {{applicationId}}
-

Dear {{name}},

-

An interview has been scheduled for the Royal Enfield Dealership application.

-
- Level: {{level}}
- Date & Time: {{dateTime}}
- Type: {{type}}
- Location/Link: {{location}} +

ROYAL ENFIELD

+
+

Hi {{name}},

+

Your interview for the Royal Enfield dealership application ({{applicationId}}) has been scheduled.

+
+

Interview Level: {{level}}
+ Date & Time: {{dateTime}}
+ Mode/Location: {{location}}
+ Type: {{type}}

+
+

Please ensure you are available at the scheduled time. If it's a virtual interview, the link will be shared separately or is included in the location field above.

-

Please ensure you are available at the scheduled time.

diff --git a/src/emailtemplates/loa_issued.html b/src/emailtemplates/loa_issued.html new file mode 100644 index 0000000..43640c4 --- /dev/null +++ b/src/emailtemplates/loa_issued.html @@ -0,0 +1,30 @@ + + + + + + +
+

ROYAL ENFIELD

+
+

Welcome to the Family!

+

Dear {{applicantName}},

+

We are honored to issue your Letter of Appointment (LOA) for the Royal Enfield dealership ({{applicationId}}). Your dealership is now officially authorized under the code: {{dealerCode}}.

+

We look forward to a successful partnership.

+
+ View LOA +
+
+ +
+ + diff --git a/src/emailtemplates/loi_issued.html b/src/emailtemplates/loi_issued.html new file mode 100644 index 0000000..cbec568 --- /dev/null +++ b/src/emailtemplates/loi_issued.html @@ -0,0 +1,29 @@ + + + + + + +
+

ROYAL ENFIELD

+
+

Congratulations {{applicantName}}!

+

We are delighted to inform you that your Letter of Intent (LOI) for the Royal Enfield dealership has been issued for the application {{applicationId}}.

+

Please log in to the portal to view and acknowledge the document to move to the next stage.

+
+ View LOI +
+
+ +
+ + diff --git a/src/emailtemplates/non_opportunity.html b/src/emailtemplates/non_opportunity.html index fe4c771..f9b3ddc 100644 --- a/src/emailtemplates/non_opportunity.html +++ b/src/emailtemplates/non_opportunity.html @@ -1,64 +1,27 @@ - -
-
-

Royal Enfield Dealership Application

-
+

ROYAL ENFIELD

-

Dear {{applicantName}},

-

Thank you for your interest in Royal Enfield and for your application to represent our brand as an - authorized dealer.

-

We have carefully reviewed your expression of interest in relation to our current network expansion - strategy for {{location}}. At this juncture, we do not have any immediate vacancies or - planned dealership opportunities available in this specific territory.

-

However, we have successfully retained your profile in our prospective partners database. Rest assured, - our team will proactively reach out to you should a suitable opportunity materialize in this region or - if our strategic requirements in your preferred location evolve further.

-

We appreciate the time you took to share your details and your continued enthusiasm for Royal Enfield. -

-

Best regards,
Dealer Development Team
Royal Enfield

+

Dear {{applicantName}},

+

Thank you for your interest in a Royal Enfield dealership for {{location}}.

+

After careful review of your application and the current organizational requirements, we regret to inform you that we are unable to proceed with your request at this time.

+

We will keep your profile in our database for future opportunities. We wish you the very best in your future endeavors.

+

Regards,
Royal Enfield Team

- \ No newline at end of file diff --git a/src/emailtemplates/opportunity.html b/src/emailtemplates/opportunity.html index 51ecfb2..e852f80 100644 --- a/src/emailtemplates/opportunity.html +++ b/src/emailtemplates/opportunity.html @@ -2,31 +2,27 @@
-
-

Royal Enfield Dealership Opportunity

-
+

ROYAL ENFIELD

-

Dear {{applicantName}},

-

Thank you for your interest in partnering with Royal Enfield.

-

We are pleased to inform you that there is an open opportunity for a dealership in your requested location ({{location}}).

-

To proceed with your application, please complete the detailed Dealership Assessment Questionnaire by clicking the link below:

- - Start Questionnaire - -

Or copy this link to your browser:

-

{{link}}

- -

Note: No login credentials are required at this stage. Your Application ID is {{applicationId}}.

+

Hi {{applicantName}},

+

Thank you for expressing interest in a Royal Enfield dealership opportunity for {{location}}.

+

To proceed with your application, we require you to complete a mandatory business assessment questionnaire. This will help us evaluate your profile better.

+ +

Please complete this at your earliest convenience. If you have any questions, feel free to contact our development team.

+

Regards,
Royal Enfield Team