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:
laxmanhalaki 2026-04-19 23:21:12 +05:30
parent 778a3a7452
commit 4fa1898824
154 changed files with 2275 additions and 1046 deletions

208
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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
View 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();

View File

@ -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) {

View File

@ -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) {

View File

@ -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({

View 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.');

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View 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(/&amp;lt;/gi, '<')
.replace(/&amp;gt;/gi, '>')
.replace(/&amp;quot;/gi, '"')
.replace(/&amp;#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&amp;/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);
}

View File

@ -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'
}); });
}; };

View File

@ -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}`);

View 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
};
}

View 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(' ');
};

View File

@ -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 v1v5 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 };

View 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));
}

View 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());
}

View File

@ -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({

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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];

View File

@ -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'

View File

@ -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;

View File

@ -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;

View File

@ -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