major code refactotoring is done all modules coverd also added the email 20+ email tamplates with rich text editor edit code is almost stable
This commit is contained in:
parent
778a3a7452
commit
4fa1898824
208
package-lock.json
generated
208
package-lock.json
generated
@ -25,6 +25,7 @@
|
|||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^7.0.12",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
|
"sanitize-html": "^2.17.3",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
@ -42,6 +43,7 @@
|
|||||||
"@types/node": "^25.0.9",
|
"@types/node": "^25.0.9",
|
||||||
"@types/nodemailer": "^7.0.5",
|
"@types/nodemailer": "^7.0.5",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
|
"@types/sanitize-html": "^2.16.1",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/validator": "^13.15.10",
|
"@types/validator": "^13.15.10",
|
||||||
@ -3411,6 +3413,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sanitize-html": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"htmlparser2": "^10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/send": {
|
"node_modules/@types/send": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||||
@ -4887,7 +4899,6 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@ -4962,6 +4973,73 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dom-serializer/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.2.3",
|
"version": "17.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
@ -5132,6 +5210,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/error-ex": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||||
@ -5960,6 +6050,25 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/htmlparser2": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
|
"entities": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http_ece": {
|
"node_modules/http_ece": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
|
||||||
@ -6253,6 +6362,15 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-plain-object": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-promise": {
|
"node_modules/is-promise": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
@ -7548,6 +7666,24 @@
|
|||||||
"node": ">= 10.16.0"
|
"node": ">= 10.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/napi-postinstall": {
|
"node_modules/napi-postinstall": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||||
@ -7900,6 +8036,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@ -8078,7 +8220,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
@ -8117,6 +8258,34 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||||
|
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postgres-array": {
|
"node_modules/postgres-array": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||||
@ -8438,6 +8607,32 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sanitize-html": {
|
||||||
|
"version": "2.17.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz",
|
||||||
|
"integrity": "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^10.1.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@ -8938,6 +9133,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-support": {
|
"node_modules/source-map-support": {
|
||||||
"version": "0.5.13",
|
"version": "0.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
|
||||||
|
|||||||
@ -55,6 +55,7 @@
|
|||||||
"nodemailer": "^7.0.12",
|
"nodemailer": "^7.0.12",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
|
"sanitize-html": "^2.17.3",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
@ -72,6 +73,7 @@
|
|||||||
"@types/node": "^25.0.9",
|
"@types/node": "^25.0.9",
|
||||||
"@types/nodemailer": "^7.0.5",
|
"@types/nodemailer": "^7.0.5",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
|
"@types/sanitize-html": "^2.16.1",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/validator": "^13.15.10",
|
"@types/validator": "^13.15.10",
|
||||||
|
|||||||
10
reset_db.ts
10
reset_db.ts
@ -29,7 +29,11 @@ async function resetAndSeed() {
|
|||||||
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
|
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
|
||||||
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
|
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
|
||||||
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
|
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
|
||||||
{ roleCode: 'ARCHITECTURE', roleName: 'Architecture Team', category: 'DEPARTMENT' }
|
{ roleCode: 'ARCHITECTURE', roleName: 'Architecture Team', category: 'DEPARTMENT' },
|
||||||
|
{ roleCode: 'FDD', roleName: 'FDD Team', category: 'EXTERNAL' },
|
||||||
|
{ roleCode: 'Legal Admin', roleName: 'Legal Admin', category: 'DEPARTMENT' },
|
||||||
|
{ roleCode: 'CEO', roleName: 'CEO', category: 'NATIONAL' },
|
||||||
|
{ roleCode: 'CCO', roleName: 'CCO', category: 'NATIONAL' }
|
||||||
];
|
];
|
||||||
for (const r of roles) await Role.create(r);
|
for (const r of roles) await Role.create(r);
|
||||||
|
|
||||||
@ -59,7 +63,9 @@ async function resetAndSeed() {
|
|||||||
{ email: 'finance@royalenfield.com', fullName: 'Finance Admin', roleCode: 'Finance', password: hashedPassword, status: 'active' },
|
{ email: 'finance@royalenfield.com', fullName: 'Finance Admin', roleCode: 'Finance', password: hashedPassword, status: 'active' },
|
||||||
{ email: 'abhishek@royalenfield.com', fullName: 'abhishek', roleCode: 'ASM', password: hashedPassword, status: 'active' },
|
{ email: 'abhishek@royalenfield.com', fullName: 'abhishek', roleCode: 'ASM', password: hashedPassword, status: 'active' },
|
||||||
{ email: 'lince@royalenfield.com', fullName: 'Lince', roleCode: 'DD Admin', password: hashedPassword, status: 'active' },
|
{ email: 'lince@royalenfield.com', fullName: 'Lince', roleCode: 'DD Admin', password: hashedPassword, status: 'active' },
|
||||||
{ email: 'legal@royalenfield.com', fullName: 'Legal Admin', roleCode: 'Legal Admin', password: hashedPassword, status: 'active' }
|
{ email: 'legal@royalenfield.com', fullName: 'Legal Admin', roleCode: 'Legal Admin', password: hashedPassword, status: 'active' },
|
||||||
|
{ email: 'ceo@royalenfield.com', fullName: 'CEO', roleCode: 'CEO', password: hashedPassword, status: 'active' },
|
||||||
|
{ email: 'cco@royalenfield.com', fullName: 'CCO', roleCode: 'CCO', password: hashedPassword, status: 'active' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
|
|||||||
31
scratch/check-models.ts
Normal file
31
scratch/check-models.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import db from '../src/database/models/index.js';
|
||||||
|
|
||||||
|
async function checkModels() {
|
||||||
|
console.log('🔍 Checking model loading...');
|
||||||
|
try {
|
||||||
|
await db.sequelize.authenticate();
|
||||||
|
console.log('✅ Database authentication successful!');
|
||||||
|
|
||||||
|
const modelsCount = Object.keys(db).filter(k => k !== 'sequelize' && k !== 'Sequelize').length;
|
||||||
|
console.log(`📊 Total models loaded: ${modelsCount}`);
|
||||||
|
|
||||||
|
// Check a few specific models
|
||||||
|
const sampleModels = ['User', 'Application', 'Dealer', 'TerminationRequest', 'AuditLog'];
|
||||||
|
for (const m of sampleModels) {
|
||||||
|
if (db[m]) {
|
||||||
|
console.log(`✅ Model ${m} is available`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Model ${m} is MISSING!`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🚀 All checks passed!');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Check failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkModels();
|
||||||
@ -68,6 +68,8 @@ async function masterReset() {
|
|||||||
{ email: 'abhishek@royalenfield.com', fullName: 'abhishek', roleCode: ROLES.ASM },
|
{ email: 'abhishek@royalenfield.com', fullName: 'abhishek', roleCode: ROLES.ASM },
|
||||||
{ email: 'lince@royalenfield.com', fullName: 'Lince', roleCode: ROLES.DD_ADMIN },
|
{ email: 'lince@royalenfield.com', fullName: 'Lince', roleCode: ROLES.DD_ADMIN },
|
||||||
{ email: 'legal@royalenfield.com', fullName: 'Legal Admin', roleCode: ROLES.LEGAL_ADMIN },
|
{ email: 'legal@royalenfield.com', fullName: 'Legal Admin', roleCode: ROLES.LEGAL_ADMIN },
|
||||||
|
{ email: 'ceo@royalenfield.com', fullName: 'CEO', roleCode: ROLES.CEO },
|
||||||
|
{ email: 'cco@royalenfield.com', fullName: 'CCO', roleCode: ROLES.CCO },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
|
|||||||
@ -24,7 +24,9 @@ async function seedUsers() {
|
|||||||
{ email: 'finance@royalenfield.com', fullName: 'Finance Admin', password: hashedPassword, roleCode: ROLES.FINANCE, status: 'active' },
|
{ email: 'finance@royalenfield.com', fullName: 'Finance Admin', password: hashedPassword, roleCode: ROLES.FINANCE, status: 'active' },
|
||||||
{ email: 'abhishek@royalenfield.com', fullName: 'abhishek', password: hashedPassword, roleCode: ROLES.ASM, status: 'active' },
|
{ email: 'abhishek@royalenfield.com', fullName: 'abhishek', password: hashedPassword, roleCode: ROLES.ASM, status: 'active' },
|
||||||
{ email: 'lince@royalenfield.com', fullName: 'Lince', password: hashedPassword, roleCode: ROLES.DD_ADMIN, status: 'active' },
|
{ email: 'lince@royalenfield.com', fullName: 'Lince', password: hashedPassword, roleCode: ROLES.DD_ADMIN, status: 'active' },
|
||||||
{ email: 'legal@royalenfield.com', fullName: 'Legal Admin', password: hashedPassword, roleCode: ROLES.LEGAL_ADMIN, status: 'active' }
|
{ email: 'legal@royalenfield.com', fullName: 'Legal Admin', password: hashedPassword, roleCode: ROLES.LEGAL_ADMIN, status: 'active' },
|
||||||
|
{ email: 'ceo@royalenfield.com', fullName: 'CEO', password: hashedPassword, roleCode: ROLES.CEO, status: 'active' },
|
||||||
|
{ email: 'cco@royalenfield.com', fullName: 'CCO', password: hashedPassword, roleCode: ROLES.CCO, status: 'active' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const u of usersToSeed) {
|
for (const u of usersToSeed) {
|
||||||
|
|||||||
@ -122,6 +122,8 @@ async function seed() {
|
|||||||
{ email: 'lince@royalenfield.com', name: 'Lince', role: 'DD Admin' },
|
{ email: 'lince@royalenfield.com', name: 'Lince', role: 'DD Admin' },
|
||||||
{ email: 'fdd@royalenfield.com', name: 'FDD Team', role: 'FDD' },
|
{ email: 'fdd@royalenfield.com', name: 'FDD Team', role: 'FDD' },
|
||||||
{ email: 'legal@royalenfield.com', name: 'Legal Admin', role: 'Legal Admin' },
|
{ email: 'legal@royalenfield.com', name: 'Legal Admin', role: 'Legal Admin' },
|
||||||
|
{ email: 'ceo@royalenfield.com', name: 'CEO', role: 'CEO' },
|
||||||
|
{ email: 'cco@royalenfield.com', name: 'CCO', role: 'CCO' },
|
||||||
];
|
];
|
||||||
for (const u of nationalUsers) {
|
for (const u of nationalUsers) {
|
||||||
const [user] = await User.findOrCreate({
|
const [user] = await User.findOrCreate({
|
||||||
|
|||||||
56
scripts/verify-standardized-offboarding.ts
Normal file
56
scripts/verify-standardized-offboarding.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
validateOffboardingAction,
|
||||||
|
getPreviousStage,
|
||||||
|
getOffboardingAuditAction
|
||||||
|
} from '../src/common/utils/offboardingWorkflow.utils.js';
|
||||||
|
import {
|
||||||
|
OFFBOARDING_ACTIONS,
|
||||||
|
REQUEST_TYPES,
|
||||||
|
TERMINATION_STAGES,
|
||||||
|
RESIGNATION_STAGES,
|
||||||
|
CONSTITUTIONAL_STAGES,
|
||||||
|
AUDIT_ACTIONS
|
||||||
|
} from '../src/common/config/constants.js';
|
||||||
|
|
||||||
|
console.log('--- Testing Standardized Offboarding Utilities ---');
|
||||||
|
|
||||||
|
// 1. Test validateOffboardingAction
|
||||||
|
console.log('Testing validateOffboardingAction...');
|
||||||
|
assert.deepEqual(validateOffboardingAction(OFFBOARDING_ACTIONS.APPROVE, ''), { valid: true });
|
||||||
|
assert.deepEqual(validateOffboardingAction(OFFBOARDING_ACTIONS.SEND_BACK, 'Short'), { valid: true }); // 'Short' is 5 chars
|
||||||
|
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.SEND_BACK, 'No').valid, false);
|
||||||
|
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.REVOKE, '').valid, false);
|
||||||
|
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.REJECT, '').valid, true); // Remarks not mandatory for reject in current util choice
|
||||||
|
console.log('✓ validateOffboardingAction passed.');
|
||||||
|
|
||||||
|
// 2. Test getPreviousStage - Termination
|
||||||
|
console.log('Testing getPreviousStage (Termination)...');
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.RBM_REVIEW), TERMINATION_STAGES.SUBMITTED);
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.ZBH_REVIEW), TERMINATION_STAGES.RBM_REVIEW);
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.TERMINATED), TERMINATION_STAGES.LEGAL_LETTER);
|
||||||
|
console.log('✓ Termination stage resolution passed.');
|
||||||
|
|
||||||
|
// 3. Test getPreviousStage - Resignation
|
||||||
|
console.log('Testing getPreviousStage (Resignation)...');
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.RBM), RESIGNATION_STAGES.ASM);
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.ZBH), RESIGNATION_STAGES.RBM);
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.COMPLETED), RESIGNATION_STAGES.FNF_INITIATED);
|
||||||
|
console.log('✓ Resignation stage resolution passed.');
|
||||||
|
|
||||||
|
// 4. Test getPreviousStage - Constitutional
|
||||||
|
console.log('Testing getPreviousStage (Constitutional)...');
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.ASM_REVIEW), CONSTITUTIONAL_STAGES.SUBMITTED);
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.ZM_RBM_REVIEW), CONSTITUTIONAL_STAGES.ASM_REVIEW);
|
||||||
|
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.COMPLETED), CONSTITUTIONAL_STAGES.LEGAL_REVIEW);
|
||||||
|
console.log('✓ Constitutional stage resolution passed.');
|
||||||
|
|
||||||
|
// 5. Test getOffboardingAuditAction mapping
|
||||||
|
console.log('Testing getOffboardingAuditAction...');
|
||||||
|
assert.equal(getOffboardingAuditAction('Sent Back', REQUEST_TYPES.TERMINATION), AUDIT_ACTIONS.UPDATED);
|
||||||
|
assert.equal(getOffboardingAuditAction('Revoke', REQUEST_TYPES.RESIGNATION), AUDIT_ACTIONS.UPDATED);
|
||||||
|
assert.equal(getOffboardingAuditAction('Approve', REQUEST_TYPES.CONSTITUTIONAL), AUDIT_ACTIONS.APPROVED);
|
||||||
|
assert.equal(getOffboardingAuditAction('REJECT', REQUEST_TYPES.TERMINATION), AUDIT_ACTIONS.REJECTED);
|
||||||
|
console.log('✓ Audit action mapping passed.');
|
||||||
|
|
||||||
|
console.log('\nALL STANDARDIZATION UTILITY CHECKS PASSED SUCCESSFULLY.');
|
||||||
@ -421,6 +421,10 @@ export const AUDIT_ACTIONS = {
|
|||||||
RESIGNATION_SUBMITTED: 'RESIGNATION_SUBMITTED',
|
RESIGNATION_SUBMITTED: 'RESIGNATION_SUBMITTED',
|
||||||
RESIGNATION_APPROVED: 'RESIGNATION_APPROVED',
|
RESIGNATION_APPROVED: 'RESIGNATION_APPROVED',
|
||||||
RESIGNATION_REJECTED: 'RESIGNATION_REJECTED',
|
RESIGNATION_REJECTED: 'RESIGNATION_REJECTED',
|
||||||
|
RESIGNATION_REVOKED: 'RESIGNATION_REVOKED',
|
||||||
|
RESIGNATION_SENT_BACK: 'RESIGNATION_SENT_BACK',
|
||||||
|
TERMINATION_REVOKED: 'TERMINATION_REVOKED',
|
||||||
|
TERMINATION_SENT_BACK: 'TERMINATION_SENT_BACK',
|
||||||
RELOCATION_SENT_BACK: 'RELOCATION_SENT_BACK',
|
RELOCATION_SENT_BACK: 'RELOCATION_SENT_BACK',
|
||||||
RELOCATION_REVOKED: 'RELOCATION_REVOKED',
|
RELOCATION_REVOKED: 'RELOCATION_REVOKED',
|
||||||
CONSTITUTIONAL_SENT_BACK: 'CONSTITUTIONAL_SENT_BACK',
|
CONSTITUTIONAL_SENT_BACK: 'CONSTITUTIONAL_SENT_BACK',
|
||||||
@ -541,6 +545,20 @@ export const REQUEST_TYPES = {
|
|||||||
TERMINATION: 'termination'
|
TERMINATION: 'termination'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Standardized Offboarding Actions
|
||||||
|
export const OFFBOARDING_ACTIONS = {
|
||||||
|
APPROVE: 'approve',
|
||||||
|
SEND_BACK: 'sendBack',
|
||||||
|
REVOKE: 'revoke',
|
||||||
|
REJECT: 'reject',
|
||||||
|
WITHDRAWAL: 'withdrawal',
|
||||||
|
ASSIGN: 'assign',
|
||||||
|
PUSH_FNF: 'pushfnf',
|
||||||
|
RECONSIDER: 'reconsider',
|
||||||
|
ISSUE_SCN: 'issueSCN',
|
||||||
|
SCN_RESPONSE: 'scnResponse'
|
||||||
|
} as const;
|
||||||
|
|
||||||
// Module List for Document Management
|
// Module List for Document Management
|
||||||
export const MODULE_LIST = ['ONBOARDING', 'RESIGNATION', 'RELOCATION', 'CONSTITUTIONAL_CHANGE', 'TERMINATION'] as const;
|
export const MODULE_LIST = ['ONBOARDING', 'RESIGNATION', 'RELOCATION', 'CONSTITUTIONAL_CHANGE', 'TERMINATION'] as const;
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,19 @@ const upload = multer({
|
|||||||
// Single file upload
|
// Single file upload
|
||||||
export const uploadSingle = upload.single('file');
|
export const uploadSingle = upload.single('file');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only parse multipart when the client sends multipart/form-data.
|
||||||
|
* Otherwise JSON bodies from express.json() are preserved — required for clearance APIs
|
||||||
|
* called with application/json (e.g. trigger-resignation.js, axios without FormData).
|
||||||
|
*/
|
||||||
|
export const uploadSingleIfMultipart = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const ct = String(req.headers['content-type'] || '').toLowerCase();
|
||||||
|
if (ct.includes('multipart/form-data')) {
|
||||||
|
return uploadSingle(req, res, next);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
// Multiple files upload
|
// Multiple files upload
|
||||||
export const uploadMultiple = upload.array('files', 10); // Max 10 files
|
export const uploadMultiple = upload.array('files', 10); // Max 10 files
|
||||||
|
|
||||||
|
|||||||
@ -52,3 +52,32 @@ export function normalizeToConstitutionalChangeType(raw: string | null | undefin
|
|||||||
const exact = ALL_CHANGE_TYPES.find((v) => v.toLowerCase() === s.toLowerCase());
|
const exact = ALL_CHANGE_TYPES.find((v) => v.toLowerCase() === s.toLowerCase());
|
||||||
return exact || null;
|
return exact || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps `constitutional_changes.changeType` → `dealers.constitutionType` when the workflow completes.
|
||||||
|
* Returns `null` when the change type does not represent a single target legal structure (skip master update).
|
||||||
|
*/
|
||||||
|
export function mapConstitutionalChangeTypeToDealerProfile(changeType: string): string | null {
|
||||||
|
const t = String(changeType || '').trim();
|
||||||
|
if (!t) return null;
|
||||||
|
|
||||||
|
if (t === CONSTITUTIONAL_CHANGE_TYPES.LLP_CONVERSION) return CONSTITUTIONAL_CHANGE_TYPES.LLP;
|
||||||
|
|
||||||
|
const structureTargets = [
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.PROPRIETORSHIP,
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.PARTNERSHIP,
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.LLP,
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.PRIVATE_LIMITED
|
||||||
|
];
|
||||||
|
if (structureTargets.includes(t as (typeof structureTargets)[number])) return t;
|
||||||
|
|
||||||
|
const skipAutoUpdate = [
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.PARTNERSHIP_CHANGE,
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.DIRECTOR_CHANGE,
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.OWNERSHIP_TRANSFER,
|
||||||
|
CONSTITUTIONAL_CHANGE_TYPES.COMPANY_FORMATION
|
||||||
|
];
|
||||||
|
if (skipAutoUpdate.includes(t as (typeof skipAutoUpdate)[number])) return null;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
90
src/common/utils/email-template-sanitize.ts
Normal file
90
src/common/utils/email-template-sanitize.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rich-text / HTML editors often encode `>` as `>`, turning `{{> partial}}` into `{{> partial}}`
|
||||||
|
* which Handlebars cannot parse. Decode entities only inside handlebars segments.
|
||||||
|
*/
|
||||||
|
export function decodeHandlebarsEntities(html: string): string {
|
||||||
|
const hb = /\{\{\{[\s\S]*?\}\}\}|\{\{[\s\S]*?\}\}/g;
|
||||||
|
return html.replace(hb, (block) =>
|
||||||
|
block
|
||||||
|
.replace(/&lt;/gi, '<')
|
||||||
|
.replace(/&gt;/gi, '>')
|
||||||
|
.replace(/&quot;/gi, '"')
|
||||||
|
.replace(/&#39;/g, "'")
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preserve `{{...}}` / `{{{...}}}` so `{{> partial}}` is not mangled by the HTML sanitizer. */
|
||||||
|
function maskHandlebars(html: string): { masked: string; tokens: string[] } {
|
||||||
|
const tokens: string[] = [];
|
||||||
|
const hb = /\{\{\{[\s\S]*?\}\}\}|\{\{[\s\S]*?\}\}/g;
|
||||||
|
let i = 0;
|
||||||
|
const masked = html.replace(hb, (m) => {
|
||||||
|
tokens.push(m);
|
||||||
|
return `__HANDLEBARS_TOKEN_${i++}__`;
|
||||||
|
});
|
||||||
|
return { masked, tokens };
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmaskHandlebars(html: string, tokens: string[]): string {
|
||||||
|
let out = html;
|
||||||
|
tokens.forEach((t, idx) => {
|
||||||
|
out = out.split(`__HANDLEBARS_TOKEN_${idx}__`).join(t);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailBodyOptions: sanitizeHtml.IOptions = {
|
||||||
|
allowedTags: [
|
||||||
|
...sanitizeHtml.defaults.allowedTags,
|
||||||
|
'img',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'table',
|
||||||
|
'thead',
|
||||||
|
'tbody',
|
||||||
|
'tr',
|
||||||
|
'td',
|
||||||
|
'th',
|
||||||
|
'caption'
|
||||||
|
],
|
||||||
|
allowedAttributes: {
|
||||||
|
...sanitizeHtml.defaults.allowedAttributes,
|
||||||
|
'*': ['style', 'class', 'align'],
|
||||||
|
a: ['href', 'name', 'target', 'style', 'class'],
|
||||||
|
img: ['src', 'alt', 'width', 'height', 'style', 'class'],
|
||||||
|
td: ['colspan', 'rowspan', 'style', 'class'],
|
||||||
|
th: ['colspan', 'rowspan', 'style', 'class'],
|
||||||
|
table: ['border', 'cellpadding', 'cellspacing', 'style', 'class', 'width']
|
||||||
|
},
|
||||||
|
allowedSchemes: ['http', 'https', 'mailto'],
|
||||||
|
allowedSchemesByTag: {
|
||||||
|
img: ['http', 'https']
|
||||||
|
},
|
||||||
|
allowProtocolRelative: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Strip dangerous markup from stored template bodies while keeping typical email HTML. */
|
||||||
|
export function sanitizeEmailTemplateBody(html: string): string {
|
||||||
|
const decoded = decodeHandlebarsEntities(html || '');
|
||||||
|
const { masked, tokens } = maskHandlebars(decoded);
|
||||||
|
const cleaned = sanitizeHtml(masked, emailBodyOptions);
|
||||||
|
return unmaskHandlebars(cleaned, tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plain-text-style subject: no HTML tags. */
|
||||||
|
export function sanitizeEmailTemplateSubject(subject: string): string {
|
||||||
|
const stripped = sanitizeHtml(subject || '', {
|
||||||
|
allowedTags: [],
|
||||||
|
allowedAttributes: {}
|
||||||
|
});
|
||||||
|
return stripped.trim().slice(0, 998);
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import path from 'path';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
import handlebars from 'handlebars';
|
import handlebars from 'handlebars';
|
||||||
|
import { registerEmailPartials, normalizeCtaPlaceholders } from './handlebars-email.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@ -38,16 +39,6 @@ initTransporter().catch(err => console.error('Failed to initialize transporter:'
|
|||||||
|
|
||||||
const { EmailTemplate } = db;
|
const { EmailTemplate } = db;
|
||||||
|
|
||||||
const readTemplate = (templateName: string, replacements: Record<string, string>) => {
|
|
||||||
const templatePath = path.join(__dirname, '../../emailtemplates', `${templateName}.html`);
|
|
||||||
if (!fs.existsSync(templatePath)) return null;
|
|
||||||
let html = fs.readFileSync(templatePath, 'utf-8');
|
|
||||||
for (const key in replacements) {
|
|
||||||
html = html.replace(new RegExp(`{{${key}}}`, 'g'), replacements[key]);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sendEmail = async (to: string, subject: string, templateCode: string, replacements: Record<string, string>) => {
|
export const sendEmail = async (to: string, subject: string, templateCode: string, replacements: Record<string, string>) => {
|
||||||
try {
|
try {
|
||||||
let finalHtml = '';
|
let finalHtml = '';
|
||||||
@ -57,32 +48,40 @@ export const sendEmail = async (to: string, subject: string, templateCode: strin
|
|||||||
const dbTemplate = await EmailTemplate.findOne({ where: { templateCode, isActive: true } });
|
const dbTemplate = await EmailTemplate.findOne({ where: { templateCode, isActive: true } });
|
||||||
|
|
||||||
if (dbTemplate) {
|
if (dbTemplate) {
|
||||||
// Prepare replacements with extra global vars
|
registerEmailPartials(handlebars);
|
||||||
const allReplacements = {
|
|
||||||
|
const allReplacements = normalizeCtaPlaceholders({
|
||||||
...replacements,
|
...replacements,
|
||||||
year: new Date().getFullYear().toString()
|
year: new Date().getFullYear().toString()
|
||||||
};
|
});
|
||||||
|
|
||||||
// Compile subject and body with data using Handlebars
|
|
||||||
const subjectTemplate = handlebars.compile(dbTemplate.subject);
|
const subjectTemplate = handlebars.compile(dbTemplate.subject);
|
||||||
finalSubject = subjectTemplate(allReplacements);
|
finalSubject = subjectTemplate(allReplacements);
|
||||||
|
|
||||||
const bodyTemplate = handlebars.compile(dbTemplate.body);
|
const bodyTemplate = handlebars.compile(dbTemplate.body);
|
||||||
finalHtml = bodyTemplate(allReplacements);
|
finalHtml = bodyTemplate(allReplacements);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to local file
|
registerEmailPartials(handlebars);
|
||||||
// Note: Local files are simple replacements for now, or could also be updated to handlebars if needed
|
const allReplacements = normalizeCtaPlaceholders({
|
||||||
// For now keeping readTemplate but we should ideally migrate local templates too if they get complex
|
|
||||||
const localHtml = readTemplate(templateCode, {
|
|
||||||
...replacements,
|
...replacements,
|
||||||
year: new Date().getFullYear().toString()
|
year: new Date().getFullYear().toString()
|
||||||
});
|
});
|
||||||
|
const templatesRoot = path.join(__dirname, '../../emailtemplates');
|
||||||
|
const lowerFile = path.join(templatesRoot, `${templateCode.toLowerCase()}.html`);
|
||||||
|
const exactFile = path.join(templatesRoot, `${templateCode}.html`);
|
||||||
|
const templatePath = fs.existsSync(lowerFile)
|
||||||
|
? lowerFile
|
||||||
|
: fs.existsSync(exactFile)
|
||||||
|
? exactFile
|
||||||
|
: null;
|
||||||
|
|
||||||
if (localHtml) {
|
if (!templatePath) {
|
||||||
finalHtml = localHtml;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Template not found: ${templateCode}`);
|
throw new Error(`Template not found: ${templateCode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const source = fs.readFileSync(templatePath, 'utf-8');
|
||||||
|
const bodyTemplate = handlebars.compile(source);
|
||||||
|
finalHtml = bodyTemplate(allReplacements);
|
||||||
}
|
}
|
||||||
|
|
||||||
const readyTransporter = await initTransporter();
|
const readyTransporter = await initTransporter();
|
||||||
@ -109,16 +108,17 @@ export const sendEmail = async (to: string, subject: string, templateCode: strin
|
|||||||
|
|
||||||
export const sendOpportunityEmail = async (to: string, applicantName: string, location: string, applicationId: string) => {
|
export const sendOpportunityEmail = async (to: string, applicantName: string, location: string, applicationId: string) => {
|
||||||
const link = `http://localhost:5173/questionnaire/${applicationId}`;
|
const link = `http://localhost:5173/questionnaire/${applicationId}`;
|
||||||
await sendEmail(to, 'Action Required: Royal Enfield Dealership Opportunity', 'opportunity', {
|
await sendEmail(to, 'Action Required: Royal Enfield Dealership Opportunity', 'OPPORTUNITY', {
|
||||||
applicantName,
|
applicantName,
|
||||||
location,
|
location,
|
||||||
applicationId,
|
applicationId,
|
||||||
link
|
link,
|
||||||
|
ctaLabel: 'Complete Questionnaire'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendNonOpportunityEmail = async (to: string, applicantName: string, location: string) => {
|
export const sendNonOpportunityEmail = async (to: string, applicantName: string, location: string) => {
|
||||||
await sendEmail(to, 'Update on your Royal Enfield Dealership Application', 'non_opportunity', {
|
await sendEmail(to, 'Update on your Royal Enfield Dealership Application', 'NON_OPPORTUNITY', {
|
||||||
applicantName,
|
applicantName,
|
||||||
location
|
location
|
||||||
});
|
});
|
||||||
@ -164,6 +164,7 @@ export const sendShortlistedEmail = async (to: string, applicantName: string, lo
|
|||||||
applicantName,
|
applicantName,
|
||||||
location,
|
location,
|
||||||
applicationId,
|
applicationId,
|
||||||
portalLink
|
portalLink,
|
||||||
|
ctaLabel: 'Visit Dealer Portal'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -94,8 +94,8 @@ export const ExternalMocksService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock SAP Financial Data Retrieval
|
* Mock SAP financial data (ad-hoc tests / future real integration).
|
||||||
* Simulates fetching outstanding dues and credit limits from SAP.
|
* F&F settlement no longer auto-creates line items from this on initiate.
|
||||||
*/
|
*/
|
||||||
mockGetFinancialDuesFromSap: async (dealerCode: string) => {
|
mockGetFinancialDuesFromSap: async (dealerCode: string) => {
|
||||||
console.log(`[MOCK SAP] Fetching financial dues for dealer: ${dealerCode}`);
|
console.log(`[MOCK SAP] Fetching financial dues for dealer: ${dealerCode}`);
|
||||||
|
|||||||
107
src/common/utils/handlebars-email.ts
Normal file
107
src/common/utils/handlebars-email.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import handlebars from 'handlebars';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
let partialsRegistered = false;
|
||||||
|
|
||||||
|
/** Resolve partials folder for both `tsx src/...` and `node dist/src/...` layouts. */
|
||||||
|
/** Inline logo so emails and preview work without public URLs or CID attachments. */
|
||||||
|
function resolveEmailHeaderLogoPath(partialsDir: string): string | null {
|
||||||
|
const candidates = [
|
||||||
|
path.join(partialsDir, '..', 'Re_Logo.png'),
|
||||||
|
path.join(process.cwd(), 'src/emailtemplates/Re_Logo.png'),
|
||||||
|
path.join(process.cwd(), 'Dealer_Onboarding_Backend/src/emailtemplates/Re_Logo.png'),
|
||||||
|
path.join(__dirname, '../../emailtemplates/Re_Logo.png'),
|
||||||
|
path.join(__dirname, '../../../../src/emailtemplates/Re_Logo.png')
|
||||||
|
];
|
||||||
|
for (const p of candidates) {
|
||||||
|
if (fs.existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectEmailHeaderLogo(html: string, partialsDir: string): string {
|
||||||
|
const logoPath = resolveEmailHeaderLogoPath(partialsDir);
|
||||||
|
if (!logoPath) {
|
||||||
|
console.warn('[handlebars-email] Re_Logo.png not found; email header will omit image.');
|
||||||
|
return html.replace(
|
||||||
|
/<img[^>]*src="__EMAIL_HEADER_LOGO_SRC__"[^>]*>/i,
|
||||||
|
'<span style="color:#e31837;font-size:22px;font-weight:bold;letter-spacing:0.05em;">Royal Enfield</span>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const b64 = fs.readFileSync(logoPath).toString('base64');
|
||||||
|
const dataUri = `data:image/png;base64,${b64}`;
|
||||||
|
return html.replace(/__EMAIL_HEADER_LOGO_SRC__/g, dataUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEmailPartialsDir(): string {
|
||||||
|
const marker = 'email_header.html';
|
||||||
|
const candidates = [
|
||||||
|
path.join(__dirname, '../../emailtemplates/partials'),
|
||||||
|
path.join(__dirname, '../../../../src/emailtemplates/partials'),
|
||||||
|
path.join(process.cwd(), 'src/emailtemplates/partials'),
|
||||||
|
path.join(process.cwd(), 'Dealer_Onboarding_Backend/src/emailtemplates/partials')
|
||||||
|
];
|
||||||
|
for (const dir of candidates) {
|
||||||
|
if (fs.existsSync(path.join(dir, marker))) return dir;
|
||||||
|
}
|
||||||
|
console.warn('[handlebars-email] Could not resolve emailtemplates/partials; tried:', candidates);
|
||||||
|
return candidates[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Register shared layout & CTA partials (idempotent). */
|
||||||
|
export function registerEmailPartials(h: typeof handlebars = handlebars): void {
|
||||||
|
if (partialsRegistered) return;
|
||||||
|
|
||||||
|
const partialsDir = resolveEmailPartialsDir();
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
email_header: 'email_header.html',
|
||||||
|
email_footer: 'email_footer.html',
|
||||||
|
primary_cta: 'primary_cta.html'
|
||||||
|
};
|
||||||
|
|
||||||
|
let loaded = 0;
|
||||||
|
for (const [name, file] of Object.entries(map)) {
|
||||||
|
const filePath = path.join(partialsDir, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
let src = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
if (name === 'email_header') {
|
||||||
|
src = injectEmailHeaderLogo(src, partialsDir);
|
||||||
|
}
|
||||||
|
h.registerPartial(name, src);
|
||||||
|
loaded++;
|
||||||
|
} else {
|
||||||
|
console.warn(`[handlebars-email] Missing partial file: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded === Object.keys(map).length) {
|
||||||
|
partialsRegistered = true;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`[handlebars-email] Only ${loaded}/${Object.keys(map).length} partials loaded from ${partialsDir}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalize CTA fields for templates using {{> primary_cta}} — URLs come from backend placeholders only. */
|
||||||
|
export function normalizeCtaPlaceholders(replacements: Record<string, string>): Record<string, string> {
|
||||||
|
const ctaUrl =
|
||||||
|
replacements.ctaUrl ||
|
||||||
|
replacements.link ||
|
||||||
|
replacements.portalLink ||
|
||||||
|
replacements.actionUrl ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
const ctaLabel = replacements.ctaLabel || 'View details';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...replacements,
|
||||||
|
ctaUrl,
|
||||||
|
ctaLabel
|
||||||
|
};
|
||||||
|
}
|
||||||
168
src/common/utils/offboardingWorkflow.utils.ts
Normal file
168
src/common/utils/offboardingWorkflow.utils.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import {
|
||||||
|
TERMINATION_STAGES,
|
||||||
|
RESIGNATION_STAGES,
|
||||||
|
CONSTITUTIONAL_STAGES,
|
||||||
|
RELOCATION_STAGES,
|
||||||
|
REQUEST_TYPES,
|
||||||
|
OFFBOARDING_ACTIONS,
|
||||||
|
AUDIT_ACTIONS
|
||||||
|
} from '../config/constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the previous stage for a given offboarding module and its current stage.
|
||||||
|
* Used for Send Back actions to determine where to roll back the workflow.
|
||||||
|
*/
|
||||||
|
export const getPreviousStage = (requestType: string, currentStage: string): string | null => {
|
||||||
|
switch (requestType) {
|
||||||
|
case REQUEST_TYPES.TERMINATION: {
|
||||||
|
const flow: Record<string, string> = {
|
||||||
|
[TERMINATION_STAGES.RBM_REVIEW]: TERMINATION_STAGES.SUBMITTED,
|
||||||
|
[TERMINATION_STAGES.ZBH_REVIEW]: TERMINATION_STAGES.RBM_REVIEW,
|
||||||
|
[TERMINATION_STAGES.DD_LEAD_REVIEW]: TERMINATION_STAGES.ZBH_REVIEW,
|
||||||
|
[TERMINATION_STAGES.LEGAL_VERIFICATION]: TERMINATION_STAGES.DD_LEAD_REVIEW,
|
||||||
|
[TERMINATION_STAGES.DD_HEAD_REVIEW]: TERMINATION_STAGES.LEGAL_VERIFICATION,
|
||||||
|
[TERMINATION_STAGES.NBH_EVALUATION]: TERMINATION_STAGES.DD_HEAD_REVIEW,
|
||||||
|
[TERMINATION_STAGES.SCN_ISSUED]: TERMINATION_STAGES.NBH_EVALUATION,
|
||||||
|
[TERMINATION_STAGES.PERSONAL_HEARING]: TERMINATION_STAGES.SCN_ISSUED,
|
||||||
|
[TERMINATION_STAGES.NBH_FINAL_APPROVAL]: TERMINATION_STAGES.PERSONAL_HEARING,
|
||||||
|
[TERMINATION_STAGES.CCO_APPROVAL]: TERMINATION_STAGES.NBH_FINAL_APPROVAL,
|
||||||
|
[TERMINATION_STAGES.CEO_APPROVAL]: TERMINATION_STAGES.CCO_APPROVAL,
|
||||||
|
[TERMINATION_STAGES.LEGAL_LETTER]: TERMINATION_STAGES.CEO_APPROVAL,
|
||||||
|
[TERMINATION_STAGES.TERMINATED]: TERMINATION_STAGES.LEGAL_LETTER
|
||||||
|
};
|
||||||
|
return flow[currentStage] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
case REQUEST_TYPES.RESIGNATION: {
|
||||||
|
const flow: Record<string, string> = {
|
||||||
|
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ASM,
|
||||||
|
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.RBM,
|
||||||
|
[RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.ZBH,
|
||||||
|
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_LEAD,
|
||||||
|
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.NBH,
|
||||||
|
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.DD_ADMIN,
|
||||||
|
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.LEGAL,
|
||||||
|
[RESIGNATION_STAGES.COMPLETED]: RESIGNATION_STAGES.FNF_INITIATED
|
||||||
|
};
|
||||||
|
return flow[currentStage] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
case REQUEST_TYPES.CONSTITUTIONAL: {
|
||||||
|
const flow: Record<string, string> = {
|
||||||
|
[CONSTITUTIONAL_STAGES.ZM_RBM_REVIEW]: CONSTITUTIONAL_STAGES.ASM_REVIEW,
|
||||||
|
|
||||||
|
[CONSTITUTIONAL_STAGES.ZBH_REVIEW]: CONSTITUTIONAL_STAGES.ZM_RBM_REVIEW,
|
||||||
|
[CONSTITUTIONAL_STAGES.LEAD_REVIEW]: CONSTITUTIONAL_STAGES.ZBH_REVIEW,
|
||||||
|
[CONSTITUTIONAL_STAGES.HEAD_REVIEW]: CONSTITUTIONAL_STAGES.LEAD_REVIEW,
|
||||||
|
[CONSTITUTIONAL_STAGES.NBH_APPROVAL]: CONSTITUTIONAL_STAGES.HEAD_REVIEW,
|
||||||
|
[CONSTITUTIONAL_STAGES.LEGAL_REVIEW]: CONSTITUTIONAL_STAGES.NBH_APPROVAL,
|
||||||
|
[CONSTITUTIONAL_STAGES.COMPLETED]: CONSTITUTIONAL_STAGES.LEGAL_REVIEW
|
||||||
|
};
|
||||||
|
return flow[currentStage] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
case REQUEST_TYPES.RELOCATION: {
|
||||||
|
const flow: Record<string, string> = {
|
||||||
|
[RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.ASM_REVIEW,
|
||||||
|
[RELOCATION_STAGES.DD_ZM_REVIEW]: RELOCATION_STAGES.RBM_REVIEW,
|
||||||
|
[RELOCATION_STAGES.ZBH_REVIEW]: RELOCATION_STAGES.DD_ZM_REVIEW,
|
||||||
|
[RELOCATION_STAGES.DD_LEAD_REVIEW]: RELOCATION_STAGES.ZBH_REVIEW,
|
||||||
|
[RELOCATION_STAGES.DD_HEAD_APPROVAL]: RELOCATION_STAGES.DD_LEAD_REVIEW,
|
||||||
|
[RELOCATION_STAGES.NBH_APPROVAL]: RELOCATION_STAGES.DD_HEAD_APPROVAL,
|
||||||
|
[RELOCATION_STAGES.LEGAL_CLEARANCE]: RELOCATION_STAGES.NBH_APPROVAL,
|
||||||
|
[RELOCATION_STAGES.NBH_CLEARANCE_EOR]: RELOCATION_STAGES.LEGAL_CLEARANCE
|
||||||
|
};
|
||||||
|
return flow[currentStage] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the common offboarding action payload.
|
||||||
|
* Standardizes the mandatory remarks check for Send Back and Revoke actions.
|
||||||
|
*/
|
||||||
|
export const validateOffboardingAction = (action: string, remarks: string): { valid: boolean; message?: string } => {
|
||||||
|
const actionLower = action.toLowerCase();
|
||||||
|
const isSendBack = actionLower === OFFBOARDING_ACTIONS.SEND_BACK.toLowerCase() || actionLower.includes('send') && actionLower.includes('back');
|
||||||
|
const isRevoke = actionLower === OFFBOARDING_ACTIONS.REVOKE.toLowerCase();
|
||||||
|
|
||||||
|
if ((isSendBack || isRevoke) && (!remarks || remarks.trim().length < 5)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Mandatory remarks (min 5 characters) required for ${isSendBack ? 'Send Back' : 'Revoke'} action.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps offboarding action to system AUDIT_ACTIONS
|
||||||
|
*/
|
||||||
|
export const getOffboardingAuditAction = (action: string, requestType: string): string => {
|
||||||
|
const actionLower = action.toLowerCase();
|
||||||
|
|
||||||
|
// 1. Direct intent mapping (Deterministic)
|
||||||
|
if (actionLower === OFFBOARDING_ACTIONS.REJECT.toLowerCase()) return AUDIT_ACTIONS.REJECTED;
|
||||||
|
if (actionLower === OFFBOARDING_ACTIONS.APPROVE.toLowerCase()) return AUDIT_ACTIONS.APPROVED;
|
||||||
|
if (actionLower === OFFBOARDING_ACTIONS.REVOKE.toLowerCase()) {
|
||||||
|
switch (requestType) {
|
||||||
|
case REQUEST_TYPES.RESIGNATION: return AUDIT_ACTIONS.RESIGNATION_REVOKED;
|
||||||
|
case REQUEST_TYPES.TERMINATION: return AUDIT_ACTIONS.TERMINATION_REVOKED;
|
||||||
|
case REQUEST_TYPES.RELOCATION: return AUDIT_ACTIONS.RELOCATION_REVOKED;
|
||||||
|
case REQUEST_TYPES.CONSTITUTIONAL: return AUDIT_ACTIONS.CONSTITUTIONAL_REVOKED;
|
||||||
|
default: return AUDIT_ACTIONS.UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actionLower === OFFBOARDING_ACTIONS.SEND_BACK.toLowerCase() || actionLower === OFFBOARDING_ACTIONS.RECONSIDER.toLowerCase()) {
|
||||||
|
switch (requestType) {
|
||||||
|
case REQUEST_TYPES.RESIGNATION: return AUDIT_ACTIONS.RESIGNATION_SENT_BACK;
|
||||||
|
case REQUEST_TYPES.TERMINATION: return AUDIT_ACTIONS.TERMINATION_SENT_BACK;
|
||||||
|
case REQUEST_TYPES.RELOCATION: return AUDIT_ACTIONS.RELOCATION_SENT_BACK;
|
||||||
|
case REQUEST_TYPES.CONSTITUTIONAL: return AUDIT_ACTIONS.CONSTITUTIONAL_SENT_BACK;
|
||||||
|
default: return AUDIT_ACTIONS.UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to descriptive scanning (Legacy/Edge cases)
|
||||||
|
if (actionLower.includes('reject')) return AUDIT_ACTIONS.REJECTED;
|
||||||
|
if (actionLower.includes('revok')) {
|
||||||
|
switch (requestType) {
|
||||||
|
case REQUEST_TYPES.TERMINATION: return AUDIT_ACTIONS.TERMINATION_REVOKED;
|
||||||
|
default: return AUDIT_ACTIONS.UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actionLower.includes('send back') || actionLower.includes('sent back') || actionLower.includes('reconsider')) {
|
||||||
|
switch (requestType) {
|
||||||
|
case REQUEST_TYPES.TERMINATION: return AUDIT_ACTIONS.TERMINATION_SENT_BACK;
|
||||||
|
default: return AUDIT_ACTIONS.UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (actionLower.includes('approval') || actionLower.includes('approve')) ? AUDIT_ACTIONS.APPROVED : AUDIT_ACTIONS.UPDATED;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats offboarding actions and stages for user-friendly display (Title Case).
|
||||||
|
* Replaces underscores/hyphens with spaces and preserves specific abbreviations.
|
||||||
|
*/
|
||||||
|
export const formatOffboardingAction = (label: string): string => {
|
||||||
|
if (!label) return '';
|
||||||
|
|
||||||
|
// 1. Clean up separators and normalize case
|
||||||
|
let formatted = label.replace(/[_-]/g, ' ').toLowerCase();
|
||||||
|
|
||||||
|
// 2. Identify tokens and preserve known abbreviations
|
||||||
|
const abbreviations = ['CCO', 'NBH', 'CEO', 'RBM', 'ZBH', 'ASM', 'LOA', 'SCN', 'DD', 'ZM', 'FNF'];
|
||||||
|
|
||||||
|
return formatted.split(' ').map(word => {
|
||||||
|
const upperWord = word.toUpperCase();
|
||||||
|
if (abbreviations.includes(upperWord)) return upperWord;
|
||||||
|
|
||||||
|
// Capitalize first letter
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||||
|
}).join(' ');
|
||||||
|
};
|
||||||
@ -2,8 +2,26 @@ import { Op } from 'sequelize';
|
|||||||
import { REQUEST_TYPES } from '../config/constants.js';
|
import { REQUEST_TYPES } from '../config/constants.js';
|
||||||
|
|
||||||
type DbLike = Record<string, any>;
|
type DbLike = Record<string, any>;
|
||||||
|
|
||||||
|
/** RFC-style UUID v1–v5 shape; same rule used inside {@link resolveEntityUuidByType}. */
|
||||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
|
/** Use before binding to UUID-typed columns (e.g. `worknotes.request_id`) — human codes like REL-xxx / CC-xxx must not be passed raw. */
|
||||||
|
export function isEntityUuidString(id: string | undefined | null): boolean {
|
||||||
|
return UUID_REGEX.test(String(id || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distinct UUID strings safe for FK lookups on `worknotes.request_id` after {@link resolveEntityUuidByType}.
|
||||||
|
* Drops human-readable business IDs when the resolved row exists (resolvedId is UUID).
|
||||||
|
*/
|
||||||
|
export function uuidCandidatesForWorknoteRequestId(
|
||||||
|
rawId: string | undefined | null,
|
||||||
|
resolvedId: string | undefined | null
|
||||||
|
): string[] {
|
||||||
|
return [...new Set([String(rawId || '').trim(), String(resolvedId || '').trim()].filter(Boolean))].filter(isEntityUuidString);
|
||||||
|
}
|
||||||
|
|
||||||
const TYPE_ALIASES: Record<string, string> = {
|
const TYPE_ALIASES: Record<string, string> = {
|
||||||
application: 'application',
|
application: 'application',
|
||||||
onboarding: 'application',
|
onboarding: 'application',
|
||||||
@ -50,7 +68,7 @@ export async function resolveEntityUuidByType(
|
|||||||
const cfg = LOOKUP_CONFIG[normalizedType];
|
const cfg = LOOKUP_CONFIG[normalizedType];
|
||||||
if (!cfg || !db?.[cfg.model]) return { resolvedId: id, normalizedType };
|
if (!cfg || !db?.[cfg.model]) return { resolvedId: id, normalizedType };
|
||||||
|
|
||||||
const isUuid = UUID_REGEX.test(id);
|
const isUuid = isEntityUuidString(id);
|
||||||
const where = isUuid
|
const where = isUuid
|
||||||
? { [Op.or]: [{ id }, { [cfg.codeField]: id }] }
|
? { [Op.or]: [{ id }, { [cfg.codeField]: id }] }
|
||||||
: { [cfg.codeField]: id };
|
: { [cfg.codeField]: id };
|
||||||
|
|||||||
146
src/common/utils/workflow-email-notifications.ts
Normal file
146
src/common/utils/workflow-email-notifications.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import db from '../../database/models/index.js';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
import { sendEmail } from './email.service.js';
|
||||||
|
import { NotificationService } from '../../services/NotificationService.js';
|
||||||
|
import { REQUEST_TYPES } from '../config/constants.js';
|
||||||
|
|
||||||
|
const { RequestParticipant, User, Outlet, District } = db;
|
||||||
|
|
||||||
|
const frontendBase = () => process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||||
|
|
||||||
|
/** Dealer acknowledgement + internal reviewers after resignation is created. */
|
||||||
|
export async function notifyResignationSubmittedEmails(resignation: any): Promise<void> {
|
||||||
|
const dealerUser = await User.findByPk(resignation.dealerId, {
|
||||||
|
attributes: ['id', 'email', 'fullName']
|
||||||
|
});
|
||||||
|
if (!dealerUser?.email) return;
|
||||||
|
|
||||||
|
const base = frontendBase();
|
||||||
|
const resignationCode = resignation.resignationId || resignation.id;
|
||||||
|
const lwd =
|
||||||
|
resignation.lastOperationalDateSales ||
|
||||||
|
resignation.lastOperationalDateServices ||
|
||||||
|
'As per application';
|
||||||
|
const dealerName = dealerUser.fullName || 'Dealer';
|
||||||
|
|
||||||
|
await sendEmail(
|
||||||
|
dealerUser.email,
|
||||||
|
`We received your resignation request — ${resignationCode}`,
|
||||||
|
'RESIGNATION_RECEIVED',
|
||||||
|
{
|
||||||
|
dealerName,
|
||||||
|
resignationId: resignationCode,
|
||||||
|
lwd: String(lwd),
|
||||||
|
link: `${base}/dealer-resignation/${resignation.id}`,
|
||||||
|
ctaLabel: 'View request'
|
||||||
|
}
|
||||||
|
).catch((err) => console.error('[notifyResignationSubmittedEmails] dealer ack:', err));
|
||||||
|
|
||||||
|
const participants = await RequestParticipant.findAll({
|
||||||
|
where: {
|
||||||
|
requestId: resignation.id,
|
||||||
|
requestType: REQUEST_TYPES.RESIGNATION,
|
||||||
|
userId: { [Op.ne]: resignation.dealerId }
|
||||||
|
},
|
||||||
|
include: [{ model: User, as: 'user', attributes: ['id', 'email', 'fullName'] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const internalLink = `${base}/resignation/${resignation.id}`;
|
||||||
|
for (const p of participants) {
|
||||||
|
const u = (p as any).user;
|
||||||
|
if (!u?.email) continue;
|
||||||
|
await NotificationService.notify(u.id, u.email, {
|
||||||
|
title: `New resignation request: ${resignationCode}`,
|
||||||
|
message: `Submitted by ${dealerName}.`,
|
||||||
|
channels: ['email', 'system'],
|
||||||
|
templateCode: 'RESIGNATION_SUBMITTED',
|
||||||
|
placeholders: {
|
||||||
|
dealerName,
|
||||||
|
resignationId: resignationCode,
|
||||||
|
lwd: String(lwd),
|
||||||
|
link: internalLink,
|
||||||
|
ctaLabel: 'Review resignation'
|
||||||
|
}
|
||||||
|
}).catch((err) => console.error('[notifyResignationSubmittedEmails] internal:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Internal reviewers after constitutional request is created. */
|
||||||
|
export async function notifyConstitutionalSubmittedEmails(request: any, dealerDisplayName: string): Promise<void> {
|
||||||
|
const participants = await RequestParticipant.findAll({
|
||||||
|
where: {
|
||||||
|
requestId: request.id,
|
||||||
|
requestType: REQUEST_TYPES.CONSTITUTIONAL,
|
||||||
|
userId: { [Op.ne]: request.dealerId }
|
||||||
|
},
|
||||||
|
include: [{ model: User, as: 'user', attributes: ['id', 'email', 'fullName'] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
const base = frontendBase();
|
||||||
|
const link = `${base}/constitutional-change/${request.id}`;
|
||||||
|
|
||||||
|
for (const p of participants) {
|
||||||
|
const u = (p as any).user;
|
||||||
|
if (!u?.email) continue;
|
||||||
|
await NotificationService.notify(u.id, u.email, {
|
||||||
|
title: `New constitutional change request: ${request.requestId}`,
|
||||||
|
message: `${dealerDisplayName} submitted a request.`,
|
||||||
|
channels: ['email', 'system'],
|
||||||
|
templateCode: 'CONSTITUTIONAL_CHANGE_SUBMITTED',
|
||||||
|
placeholders: {
|
||||||
|
dealerName: dealerDisplayName,
|
||||||
|
changeType: request.changeType || '',
|
||||||
|
requestId: request.requestId,
|
||||||
|
link,
|
||||||
|
ctaLabel: 'Review request'
|
||||||
|
}
|
||||||
|
}).catch((err) => console.error('[notifyConstitutionalSubmittedEmails]:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dealer + ASM when relocation request is submitted. */
|
||||||
|
export async function notifyRelocationSubmittedEmails(
|
||||||
|
request: any,
|
||||||
|
submitter: { email: string; fullName?: string | null }
|
||||||
|
): Promise<void> {
|
||||||
|
const base = frontendBase();
|
||||||
|
const code = request.requestId || request.id;
|
||||||
|
const dealerName = submitter.fullName?.trim() || 'Dealer';
|
||||||
|
|
||||||
|
if (submitter.email) {
|
||||||
|
await sendEmail(
|
||||||
|
submitter.email,
|
||||||
|
`Relocation request received — ${code}`,
|
||||||
|
'RELOCATION_RECEIVED',
|
||||||
|
{
|
||||||
|
dealerName,
|
||||||
|
requestId: code,
|
||||||
|
link: `${base}/relocation-requests/${request.id}`,
|
||||||
|
ctaLabel: 'View request'
|
||||||
|
}
|
||||||
|
).catch((err) => console.error('[notifyRelocationSubmittedEmails] dealer:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
const outlet = await Outlet.findByPk(request.outletId, {
|
||||||
|
include: [{ model: District, as: 'district', attributes: ['id', 'asmId'] }]
|
||||||
|
});
|
||||||
|
const asmId = (outlet as any)?.district?.asmId;
|
||||||
|
if (!asmId) return;
|
||||||
|
|
||||||
|
const asm = await User.findByPk(asmId, { attributes: ['id', 'email', 'fullName'] });
|
||||||
|
if (!asm?.email) return;
|
||||||
|
|
||||||
|
await NotificationService.notify(asm.id, asm.email, {
|
||||||
|
title: `New relocation request: ${code}`,
|
||||||
|
message: 'A dealer submitted an outlet relocation request.',
|
||||||
|
channels: ['email', 'system'],
|
||||||
|
templateCode: 'RELOCATION_SUBMITTED',
|
||||||
|
placeholders: {
|
||||||
|
dealerName,
|
||||||
|
requestId: code,
|
||||||
|
outletCode: outlet?.code || '',
|
||||||
|
link: `${base}/relocation-requests/${request.id}`,
|
||||||
|
ctaLabel: 'Review relocation'
|
||||||
|
}
|
||||||
|
}).catch((err) => console.error('[notifyRelocationSubmittedEmails] ASM:', err));
|
||||||
|
}
|
||||||
38
src/constants/allowed-email-template-codes.ts
Normal file
38
src/constants/allowed-email-template-codes.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Allowed email template trigger codes — must match workflows and seed-master-emails.ts.
|
||||||
|
*/
|
||||||
|
export const ALLOWED_EMAIL_TEMPLATE_CODES = [
|
||||||
|
'APPLICANT_SHORTLISTED',
|
||||||
|
'CONSTITUTIONAL_CHANGE_SUBMITTED',
|
||||||
|
'CONSTITUTIONAL_CHANGE_UPDATE',
|
||||||
|
'DEALER_CODE_READY',
|
||||||
|
'GENERIC_NOTIFICATION',
|
||||||
|
'INTERVIEW_SCHEDULED',
|
||||||
|
'LOA_ISSUED',
|
||||||
|
'LOI_ISSUED',
|
||||||
|
'NON_OPPORTUNITY',
|
||||||
|
'ONBOARDING_STATUS_UPDATE',
|
||||||
|
'OPPORTUNITY',
|
||||||
|
'QUESTIONNAIRE_REMINDER',
|
||||||
|
'QUESTIONNAIRE_SUBMITTED',
|
||||||
|
'RELOCATION_RECEIVED',
|
||||||
|
'RELOCATION_SUBMITTED',
|
||||||
|
'RELOCATION_UPDATE',
|
||||||
|
'RESIGNATION_APPROVED',
|
||||||
|
'RESIGNATION_RECEIVED',
|
||||||
|
'RESIGNATION_SUBMITTED',
|
||||||
|
'RESIGNATION_UPDATE',
|
||||||
|
'SLA_BREACH_WARNING',
|
||||||
|
'TERMINATION_SCN_ISSUED',
|
||||||
|
'TERMINATION_UPDATE',
|
||||||
|
'USER_ASSIGNED',
|
||||||
|
'WORKNOTE_NOTIFICATION'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type AllowedEmailTemplateCode = (typeof ALLOWED_EMAIL_TEMPLATE_CODES)[number];
|
||||||
|
|
||||||
|
const ALLOWED_SET = new Set<string>(ALLOWED_EMAIL_TEMPLATE_CODES);
|
||||||
|
|
||||||
|
export function isAllowedEmailTemplateCode(code: string): boolean {
|
||||||
|
return ALLOWED_SET.has(code.trim().toUpperCase());
|
||||||
|
}
|
||||||
@ -1,9 +1,22 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
import handlebars from 'handlebars';
|
import handlebars from 'handlebars';
|
||||||
|
import { registerEmailPartials, normalizeCtaPlaceholders } from '../../common/utils/handlebars-email.js';
|
||||||
|
import { sanitizeEmailTemplateBody, sanitizeEmailTemplateSubject } from '../../common/utils/email-template-sanitize.js';
|
||||||
|
import { isAllowedEmailTemplateCode } from '../../constants/allowed-email-template-codes.js';
|
||||||
|
|
||||||
const { EmailTemplate } = db;
|
const { EmailTemplate } = db;
|
||||||
|
|
||||||
|
const editableTemplateFields = ['templateCode', 'description', 'subject', 'body', 'placeholders', 'isActive'] as const;
|
||||||
|
|
||||||
|
function pickEditableTemplatePayload(body: Record<string, unknown>) {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const key of editableTemplateFields) {
|
||||||
|
if (key in body) out[key] = body[key];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export const EmailTemplateController = {
|
export const EmailTemplateController = {
|
||||||
// Get all templates
|
// Get all templates
|
||||||
getAllTemplates: async (req: Request, res: Response) => {
|
getAllTemplates: async (req: Request, res: Response) => {
|
||||||
@ -38,7 +51,22 @@ export const EmailTemplateController = {
|
|||||||
// Create template
|
// Create template
|
||||||
createTemplate: async (req: Request, res: Response) => {
|
createTemplate: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const template = await EmailTemplate.create(req.body);
|
const payload = pickEditableTemplatePayload(req.body as Record<string, unknown>);
|
||||||
|
if (typeof payload.body === 'string') payload.body = sanitizeEmailTemplateBody(payload.body);
|
||||||
|
if (typeof payload.subject === 'string') payload.subject = sanitizeEmailTemplateSubject(payload.subject);
|
||||||
|
|
||||||
|
const rawCode = payload.templateCode;
|
||||||
|
const normalized =
|
||||||
|
typeof rawCode === 'string' ? rawCode.trim().toUpperCase() : '';
|
||||||
|
if (!normalized || !isAllowedEmailTemplateCode(normalized)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'templateCode must be one of the system-defined trigger codes.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload.templateCode = normalized;
|
||||||
|
|
||||||
|
const template = await EmailTemplate.create(payload);
|
||||||
res.status(201).json({ success: true, data: template, message: 'Template created successfully' });
|
res.status(201).json({ success: true, data: template, message: 'Template created successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating template:', error);
|
console.error('Error creating template:', error);
|
||||||
@ -50,7 +78,30 @@ export const EmailTemplateController = {
|
|||||||
updateTemplate: async (req: Request, res: Response) => {
|
updateTemplate: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const [updated] = await EmailTemplate.update(req.body, {
|
const existing = await EmailTemplate.findByPk(id);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Template not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = pickEditableTemplatePayload(req.body as Record<string, unknown>);
|
||||||
|
if (typeof payload.body === 'string') payload.body = sanitizeEmailTemplateBody(payload.body);
|
||||||
|
if (typeof payload.subject === 'string') payload.subject = sanitizeEmailTemplateSubject(payload.subject);
|
||||||
|
|
||||||
|
if ('templateCode' in payload && payload.templateCode !== undefined) {
|
||||||
|
const raw = payload.templateCode;
|
||||||
|
const normalized =
|
||||||
|
typeof raw === 'string' ? raw.trim().toUpperCase() : '';
|
||||||
|
const previous = existing.templateCode.trim().toUpperCase();
|
||||||
|
if (normalized !== previous && !isAllowedEmailTemplateCode(normalized)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'templateCode must be one of the system-defined trigger codes.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload.templateCode = normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await EmailTemplate.update(payload, {
|
||||||
where: { id }
|
where: { id }
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,15 +149,23 @@ export const EmailTemplateController = {
|
|||||||
let compiledSubject = subject;
|
let compiledSubject = subject;
|
||||||
let compiledBody = body;
|
let compiledBody = body;
|
||||||
|
|
||||||
const safeData = data || {};
|
registerEmailPartials(handlebars);
|
||||||
|
|
||||||
|
const safeBody = sanitizeEmailTemplateBody(body);
|
||||||
|
const safeSubject = subject ? sanitizeEmailTemplateSubject(subject) : '';
|
||||||
|
|
||||||
|
const safeData = normalizeCtaPlaceholders({
|
||||||
|
...(data || {}),
|
||||||
|
year: new Date().getFullYear().toString()
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (subject) {
|
if (subject) {
|
||||||
const subjectTemplate = handlebars.compile(subject);
|
const subjectTemplate = handlebars.compile(safeSubject);
|
||||||
compiledSubject = subjectTemplate(safeData);
|
compiledSubject = subjectTemplate(safeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bodyTemplate = handlebars.compile(body);
|
const bodyTemplate = handlebars.compile(safeBody);
|
||||||
compiledBody = bodyTemplate(safeData);
|
compiledBody = bodyTemplate(safeData);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS } from '../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface AuditLogAttributes {
|
export interface AuditLogAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } from '../../common/config/constants.js';
|
import { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } from '../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface ApplicationAttributes {
|
export interface ApplicationAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { REQUEST_TYPES, DOCUMENT_TYPES } from '../../common/config/constants.js';
|
import { REQUEST_TYPES, DOCUMENT_TYPES } from '../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface DocumentAttributes {
|
export interface DocumentAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { ROLES, REGIONS } from '../../common/config/constants.js';
|
import { ROLES, REGIONS } from '../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface UserAttributes {
|
export interface UserAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { OUTLET_TYPES, OUTLET_STATUS, REGIONS } from '../../common/config/constants.js';
|
import { OUTLET_TYPES, OUTLET_STATUS, REGIONS } from '../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface OutletAttributes {
|
export interface OutletAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { PAYMENT_TYPES, PAYMENT_STATUS } from '../../common/config/constants.js';
|
import { PAYMENT_TYPES, PAYMENT_STATUS } from '../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface FinancePaymentAttributes {
|
export interface FinancePaymentAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { FNF_STATUS } from '../../common/config/constants.js';
|
import { FNF_STATUS } from '../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface FnFAttributes {
|
export interface FnFAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -2,104 +2,106 @@ import { Sequelize } from 'sequelize';
|
|||||||
import config from '../../common/config/database.js';
|
import config from '../../common/config/database.js';
|
||||||
|
|
||||||
// Import individual model factories
|
// Import individual model factories
|
||||||
import createUser from './User.js';
|
|
||||||
import createApplication from './Application.js';
|
|
||||||
import createResignation from './Resignation.js';
|
|
||||||
import createConstitutionalChange from './ConstitutionalChange.js';
|
|
||||||
import createRelocationRequest from './RelocationRequest.js';
|
|
||||||
import createOutlet from './Outlet.js';
|
|
||||||
import createWorknote from './Worknote.js';
|
|
||||||
import createOnboardingDocument from './OnboardingDocument.js';
|
|
||||||
import createAuditLog from './AuditLog.js';
|
|
||||||
import createFinancePayment from './FinancePayment.js';
|
|
||||||
import createRelocationDocument from './RelocationDocument.js';
|
|
||||||
import createResignationDocument from './ResignationDocument.js';
|
|
||||||
import createConstitutionalDocument from './ConstitutionalDocument.js';
|
|
||||||
import createTerminationDocument from './TerminationDocument.js';
|
|
||||||
import createFnF from './FnF.js';
|
|
||||||
import createFnFLineItem from './FnFLineItem.js';
|
|
||||||
import createSLAConfiguration from './SLAConfiguration.js';
|
|
||||||
import createSLAReminder from './SLAReminder.js';
|
|
||||||
import createSLAEscalationConfig from './SLAEscalationConfig.js';
|
|
||||||
import createWorkflowStageConfig from './WorkflowStageConfig.js';
|
|
||||||
import createSystemConfiguration from './SystemConfiguration.js';
|
|
||||||
import createDocumentStageConfig from './DocumentStageConfig.js';
|
|
||||||
import createNotification from './Notification.js';
|
|
||||||
import createDistrict from './District.js';
|
|
||||||
import createLocation from './Location.js';
|
|
||||||
import createZone from './Zone.js';
|
|
||||||
import createRegion from './Region.js';
|
|
||||||
import createState from './State.js';
|
|
||||||
import createTerminationScnResponse from './TerminationScnResponse.js';
|
|
||||||
import createTerminationHearingRecord from './TerminationHearingRecord.js';
|
|
||||||
import createFffClearance from './FffClearance.js';
|
|
||||||
import createResignationAudit from './ResignationAudit.js';
|
|
||||||
import createTerminationAudit from './TerminationAudit.js';
|
|
||||||
import createFnFAudit from './FnFAudit.js';
|
|
||||||
import createConstitutionalAudit from './ConstitutionalAudit.js';
|
|
||||||
import createRelocationAudit from './RelocationAudit.js';
|
|
||||||
|
|
||||||
// Batch 1: Organizational Hierarchy & User Management
|
// Core
|
||||||
import createRole from './Role.js';
|
import createUser from './core/User.js';
|
||||||
import createPermission from './Permission.js';
|
import createRole from './core/Role.js';
|
||||||
import createRolePermission from './RolePermission.js';
|
import createPermission from './core/Permission.js';
|
||||||
import createUserRole from './UserRole.js';
|
import createRolePermission from './core/RolePermission.js';
|
||||||
|
import createUserRole from './core/UserRole.js';
|
||||||
|
import createSystemConfiguration from './core/SystemConfiguration.js';
|
||||||
|
import createEmailTemplate from './core/EmailTemplate.js';
|
||||||
|
import createPushSubscription from './core/PushSubscription.js';
|
||||||
|
import createNotification from './core/Notification.js';
|
||||||
|
import createState from './core/State.js';
|
||||||
|
import createDistrict from './core/District.js';
|
||||||
|
import createRegion from './core/Region.js';
|
||||||
|
import createZone from './core/Zone.js';
|
||||||
|
import createLocation from './core/Location.js';
|
||||||
|
|
||||||
// Batch 2: Opportunity & Application Framework
|
// Application
|
||||||
import createOpportunity from './Opportunity.js';
|
import createApplication from './application/Application.js';
|
||||||
import createApplicationStatusHistory from './ApplicationStatusHistory.js';
|
import createApplicationProgress from './application/ApplicationProgress.js';
|
||||||
import createApplicationProgress from './ApplicationProgress.js';
|
import createApplicationStatusHistory from './application/ApplicationStatusHistory.js';
|
||||||
|
import createOpportunity from './application/Opportunity.js';
|
||||||
|
import createOnboardingDocument from './application/OnboardingDocument.js';
|
||||||
|
import createDocumentVersion from './application/DocumentVersion.js';
|
||||||
|
|
||||||
// Batch 3: Questionnaire & Interview Systems
|
// Dealer
|
||||||
import createQuestionnaire from './Questionnaire.js';
|
import createDealer from './dealer/Dealer.js';
|
||||||
import createQuestionnaireQuestion from './QuestionnaireQuestion.js';
|
import createDealerCode from './dealer/DealerCode.js';
|
||||||
import createQuestionnaireOption from './QuestionnaireOption.js';
|
import createDealerBankDetail from './dealer/DealerBankDetail.js';
|
||||||
import createQuestionnaireResponse from './QuestionnaireResponse.js';
|
import createOutlet from './dealer/Outlet.js';
|
||||||
import createQuestionnaireScore from './QuestionnaireScore.js';
|
|
||||||
import createInterview from './Interview.js';
|
|
||||||
import createInterviewParticipant from './InterviewParticipant.js';
|
|
||||||
import createInterviewEvaluation from './InterviewEvaluation.js';
|
|
||||||
import createKTMatrixScore from './KTMatrixScore.js';
|
|
||||||
import createInterviewFeedback from './InterviewFeedback.js';
|
|
||||||
import createAiSummary from './AiSummary.js';
|
|
||||||
|
|
||||||
// Batch 4: Dealer Entity, Documents & Work Notes
|
// Verification
|
||||||
import createDealer from './Dealer.js';
|
import createInterview from './verification/Interview.js';
|
||||||
import createDealerCode from './DealerCode.js';
|
import createInterviewEvaluation from './verification/InterviewEvaluation.js';
|
||||||
import createDealerBankDetail from './DealerBankDetail.js';
|
import createInterviewFeedback from './verification/InterviewFeedback.js';
|
||||||
import createDocumentVersion from './DocumentVersion.js';
|
import createInterviewParticipant from './verification/InterviewParticipant.js';
|
||||||
import createWorkNoteTag from './WorkNoteTag.js';
|
import createQuestionnaire from './verification/Questionnaire.js';
|
||||||
import createWorkNoteAttachment from './WorkNoteAttachment.js';
|
import createQuestionnaireOption from './verification/QuestionnaireOption.js';
|
||||||
import createRequestParticipant from './RequestParticipant.js';
|
import createQuestionnaireQuestion from './verification/QuestionnaireQuestion.js';
|
||||||
|
import createQuestionnaireResponse from './verification/QuestionnaireResponse.js';
|
||||||
|
import createQuestionnaireScore from './verification/QuestionnaireScore.js';
|
||||||
|
import createKTMatrixScore from './verification/KTMatrixScore.js';
|
||||||
|
import createAiSummary from './verification/AiSummary.js';
|
||||||
|
import createFddAssignment from './verification/FddAssignment.js';
|
||||||
|
import createFddReport from './verification/FddReport.js';
|
||||||
|
|
||||||
// Batch 5: FDD, LOI, LOA, EOR & Security Deposit
|
// Approval
|
||||||
import createFddAssignment from './FddAssignment.js';
|
import createLoiRequest from './approval/LoiRequest.js';
|
||||||
import createFddReport from './FddReport.js';
|
import createLoiApproval from './approval/LoiApproval.js';
|
||||||
import createLoiRequest from './LoiRequest.js';
|
import createLoiAcknowledgement from './approval/LoiAcknowledgement.js';
|
||||||
import createLoiApproval from './LoiApproval.js';
|
import createLoiDocumentGenerated from './approval/LoiDocumentGenerated.js';
|
||||||
import createLoiDocumentGenerated from './LoiDocumentGenerated.js';
|
import createLoaRequest from './approval/LoaRequest.js';
|
||||||
import createLoiAcknowledgement from './LoiAcknowledgement.js';
|
import createLoaApproval from './approval/LoaApproval.js';
|
||||||
import createSecurityDeposit from './SecurityDeposit.js';
|
import createLoaAcknowledgement from './approval/LoaAcknowledgement.js';
|
||||||
import createLoaRequest from './LoaRequest.js';
|
import createLoaDocumentGenerated from './approval/LoaDocumentGenerated.js';
|
||||||
import createLoaApproval from './LoaApproval.js';
|
import createSecurityDeposit from './approval/SecurityDeposit.js';
|
||||||
import createLoaDocumentGenerated from './LoaDocumentGenerated.js';
|
|
||||||
import createLoaAcknowledgement from './LoaAcknowledgement.js';
|
|
||||||
import createEorChecklist from './EorChecklist.js';
|
|
||||||
import createEorChecklistItem from './EorChecklistItem.js';
|
|
||||||
|
|
||||||
// Batch 6: Offboarding & F&F Settlement
|
// Offboarding
|
||||||
import createTerminationRequest from './TerminationRequest.js';
|
import createTerminationRequest from './offboarding/termination/TerminationRequest.js';
|
||||||
import createExitFeedback from './ExitFeedback.js';
|
import createTerminationAudit from './offboarding/termination/TerminationAudit.js';
|
||||||
|
import createTerminationDocument from './offboarding/termination/TerminationDocument.js';
|
||||||
|
import createTerminationHearingRecord from './offboarding/termination/TerminationHearingRecord.js';
|
||||||
|
import createTerminationScnResponse from './offboarding/termination/TerminationScnResponse.js';
|
||||||
|
import createResignation from './offboarding/resignation/Resignation.js';
|
||||||
|
import createResignationAudit from './offboarding/resignation/ResignationAudit.js';
|
||||||
|
import createResignationDocument from './offboarding/resignation/ResignationDocument.js';
|
||||||
|
import createRelocationRequest from './offboarding/relocation/RelocationRequest.js';
|
||||||
|
import createRelocationAudit from './offboarding/relocation/RelocationAudit.js';
|
||||||
|
import createRelocationDocument from './offboarding/relocation/RelocationDocument.js';
|
||||||
|
import createConstitutionalChange from './offboarding/constitutional/ConstitutionalChange.js';
|
||||||
|
import createConstitutionalAudit from './offboarding/constitutional/ConstitutionalAudit.js';
|
||||||
|
import createConstitutionalDocument from './offboarding/constitutional/ConstitutionalDocument.js';
|
||||||
|
import createExitFeedback from './offboarding/common/ExitFeedback.js';
|
||||||
|
|
||||||
// Batch 7: Notifications, Logs & Templates
|
// Financial
|
||||||
import createEmailTemplate from './EmailTemplate.js';
|
import createFnF from './financial/FnF.js';
|
||||||
import createPushSubscription from './PushSubscription.js';
|
import createFnFAudit from './financial/FnFAudit.js';
|
||||||
|
import createFnFLineItem from './financial/FnFLineItem.js';
|
||||||
|
import createFffClearance from './financial/FffClearance.js';
|
||||||
|
import createFinancePayment from './financial/FinancePayment.js';
|
||||||
|
|
||||||
// Batch 8: SLA & TAT Tracking
|
// Compliance
|
||||||
import createSLATracking from './SLATracking.js';
|
import createEorChecklist from './compliance/EorChecklist.js';
|
||||||
import createSLABreach from './SLABreach.js';
|
import createEorChecklistItem from './compliance/EorChecklistItem.js';
|
||||||
import createStageApprovalPolicy from './StageApprovalPolicy.js';
|
import createSLAConfiguration from './compliance/SLAConfiguration.js';
|
||||||
import createStageApprovalAction from './StageApprovalAction.js';
|
import createSLABreach from './compliance/SLABreach.js';
|
||||||
|
import createSLAEscalationConfig from './compliance/SLAEscalationConfig.js';
|
||||||
|
import createSLAReminder from './compliance/SLAReminder.js';
|
||||||
|
import createSLATracking from './compliance/SLATracking.js';
|
||||||
|
import createWorkflowStageConfig from './compliance/WorkflowStageConfig.js';
|
||||||
|
import createStageApprovalAction from './compliance/StageApprovalAction.js';
|
||||||
|
import createStageApprovalPolicy from './compliance/StageApprovalPolicy.js';
|
||||||
|
import createDocumentStageConfig from './compliance/DocumentStageConfig.js';
|
||||||
|
import createRequestParticipant from './compliance/RequestParticipant.js';
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
import createAuditLog from './activity/AuditLog.js';
|
||||||
|
import createWorknote from './activity/Worknote.js';
|
||||||
|
import createWorkNoteAttachment from './activity/WorkNoteAttachment.js';
|
||||||
|
import createWorkNoteTag from './activity/WorkNoteTag.js';
|
||||||
|
|
||||||
const env = process.env.NODE_ENV || 'development';
|
const env = process.env.NODE_ENV || 'development';
|
||||||
const dbConfig = config[env];
|
const dbConfig = config[env];
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { CONSTITUTIONAL_CHANGE_TYPES, CONSTITUTIONAL_STAGES } from '../../common/config/constants.js';
|
import { CONSTITUTIONAL_CHANGE_TYPES, CONSTITUTIONAL_STAGES } from '../../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface ConstitutionalChangeAttributes {
|
export interface ConstitutionalChangeAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -71,8 +71,9 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
currentStage: {
|
currentStage: {
|
||||||
type: DataTypes.ENUM(...Object.values(CONSTITUTIONAL_STAGES)),
|
type: DataTypes.ENUM(...Object.values(CONSTITUTIONAL_STAGES)),
|
||||||
defaultValue: CONSTITUTIONAL_STAGES.SUBMITTED
|
defaultValue: CONSTITUTIONAL_STAGES.ASM_REVIEW
|
||||||
},
|
},
|
||||||
|
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'Pending'
|
defaultValue: 'Pending'
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { RELOCATION_TYPES, RELOCATION_STAGES } from '../../common/config/constants.js';
|
import { RELOCATION_TYPES, RELOCATION_STAGES } from '../../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface RelocationRequestAttributes {
|
export interface RelocationRequestAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
import { RESIGNATION_TYPES, RESIGNATION_STAGES } from '../../common/config/constants.js';
|
import { RESIGNATION_TYPES, RESIGNATION_STAGES } from '../../../../common/config/constants.js';
|
||||||
|
|
||||||
export interface ResignationAttributes {
|
export interface ResignationAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
@ -21,7 +21,8 @@ export interface ResignationAttributes {
|
|||||||
departmentalClearances: Record<string, {
|
departmentalClearances: Record<string, {
|
||||||
status: 'Pending' | 'Cleared' | 'Dues';
|
status: 'Pending' | 'Cleared' | 'Dues';
|
||||||
amount?: number;
|
amount?: number;
|
||||||
type?: 'Payable' | 'Recovery';
|
/** Receivable = amount owed by dealer; Recovery kept for legacy JSON only */
|
||||||
|
type?: 'Payable' | 'Receivable' | 'Recovery';
|
||||||
remarks?: string;
|
remarks?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
updatedBy?: string;
|
updatedBy?: string;
|
||||||
@ -16,7 +16,7 @@ export interface TerminationRequestAttributes {
|
|||||||
departmentalClearances: Record<string, {
|
departmentalClearances: Record<string, {
|
||||||
status: 'Pending' | 'Cleared' | 'Dues';
|
status: 'Pending' | 'Cleared' | 'Dues';
|
||||||
amount?: number;
|
amount?: number;
|
||||||
type?: 'Payable' | 'Recovery';
|
type?: 'Payable' | 'Receivable' | 'Recovery';
|
||||||
remarks?: string;
|
remarks?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
updatedBy?: string;
|
updatedBy?: string;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user