master ata modification done removed unecesary input fileds in modal popup like emp id added request id resolver for readable id resolve

This commit is contained in:
laxman h 2026-04-15 21:10:46 +05:30
parent 86f2323641
commit 96edda54d9
18 changed files with 671 additions and 256 deletions

View File

@ -12,6 +12,7 @@
"migrate": "tsx scripts/migrate.ts", "migrate": "tsx scripts/migrate.ts",
"reset:stable": "tsx scripts/reset_db_stable.ts", "reset:stable": "tsx scripts/reset_db_stable.ts",
"seed": "tsx scripts/seed_normalized_data.ts", "seed": "tsx scripts/seed_normalized_data.ts",
"seed:roles": "tsx scripts/seed-roles.ts",
"seed:permissions": "tsx scripts/seed-permissions.ts", "seed:permissions": "tsx scripts/seed-permissions.ts",
"seed:approval-policies": "tsx scripts/seed-approval-policies.ts", "seed:approval-policies": "tsx scripts/seed-approval-policies.ts",
"seed:email-templates": "tsx src/scripts/seed-master-emails.ts", "seed:email-templates": "tsx src/scripts/seed-master-emails.ts",
@ -20,6 +21,9 @@
"seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates && npm run seed:configs && npm run seed:document-configs", "seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates && npm run seed:configs && npm run seed:document-configs",
"setup:fresh": "npm run migrate && npm run seed:real-geo && npm run seed:all && npm run sync:hierarchy", "setup:fresh": "npm run migrate && npm run seed:real-geo && npm run seed:all && npm run sync:hierarchy",
"seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy", "seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy",
"seed:state-district": "tsx scripts/seed-state-district-only.ts",
"seed:minimal-admin": "tsx scripts/seed-minimal-admin.ts",
"setup:fresh:minimal": "npm run migrate && npm run seed:roles && npm run seed:state-district && npm run seed:minimal-admin",
"sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts", "sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts",
"seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts", "seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts",
"test": "jest", "test": "jest",

View File

@ -2,6 +2,7 @@ import 'dotenv/config';
import db from '../src/database/models/index.js'; import db from '../src/database/models/index.js';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { ROLES, APPLICATION_STAGES, APPLICATION_STATUS } from '../src/common/config/constants.js'; import { ROLES, APPLICATION_STAGES, APPLICATION_STATUS } from '../src/common/config/constants.js';
import { resolveManagerCode } from '../src/services/userRoleCode.service.js';
const { Role, User, UserRole, Zone, State, Region, Location } = db; const { Role, User, UserRole, Zone, State, Region, Location } = db;
@ -84,6 +85,7 @@ async function masterReset() {
// Map assignments based on role category (Regional vs Granular) // Map assignments based on role category (Regional vs Granular)
const isRegionalRole = [ROLES.RBM, ROLES.DD_ZM, ROLES.ZBH].includes(u.roleCode as any); const isRegionalRole = [ROLES.RBM, ROLES.DD_ZM, ROLES.ZBH].includes(u.roleCode as any);
const isGranularRole = [ROLES.ASM].includes(u.roleCode as any); const isGranularRole = [ROLES.ASM].includes(u.roleCode as any);
const managerCode = await resolveManagerCode(role.id, u.roleCode as string, null);
await UserRole.create({ await UserRole.create({
userId: user.id, userId: user.id,
@ -92,7 +94,8 @@ async function masterReset() {
isPrimary: true, isPrimary: true,
zoneId: isRegionalRole || isGranularRole ? zone.id : null, zoneId: isRegionalRole || isGranularRole ? zone.id : null,
regionId: isRegionalRole ? region.id : null, regionId: isRegionalRole ? region.id : null,
districtId: isGranularRole ? district.id : null districtId: isGranularRole ? district.id : null,
managerCode
}); });
} }
} }

View File

@ -0,0 +1,85 @@
import 'dotenv/config';
import bcrypt from 'bcryptjs';
import db from '../src/database/models/index.js';
import { ROLES } from '../src/common/config/constants.js';
const ADMIN_EMAIL = 'admin@gmail.com';
const ADMIN_PASSWORD = 'Admin@123';
const ADMIN_NAME = 'System Admin';
async function seedMinimalAdmin() {
console.log('--- Seeding minimal admin-only data ---');
try {
await db.sequelize.authenticate();
const stateCount = await db.State.count();
const districtCount = await db.District.count();
const regionCount = await db.Region.count();
const zoneCount = await db.Zone.count();
if (!stateCount || !districtCount) {
throw new Error(
'Geo master data is incomplete. Run "npm run seed:state-district" before "seed:minimal-admin".'
);
}
if (!regionCount || !zoneCount) {
console.log(' Region/Zone not seeded (expected for minimal setup). You can add them manually later.');
}
// Ensure only one user remains for this minimal environment.
await db.UserRole.destroy({ where: {} });
await db.User.destroy({ where: {} });
// Ensure required roles exist.
await db.Role.findOrCreate({
where: { roleCode: ROLES.SUPER_ADMIN },
defaults: {
roleCode: ROLES.SUPER_ADMIN,
roleName: 'Super Admin',
category: 'ADMIN',
description: 'Full system access'
}
});
await db.Role.findOrCreate({
where: { roleCode: ROLES.DD_ADMIN },
defaults: {
roleCode: ROLES.DD_ADMIN,
roleName: 'DD Admin',
category: 'ADMIN',
description: 'Dealer Development Admin'
}
});
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
const adminUser = await db.User.create({
email: ADMIN_EMAIL,
fullName: ADMIN_NAME,
password: hashedPassword,
roleCode: ROLES.SUPER_ADMIN,
status: 'active',
isActive: true,
isExternal: false
});
const role = await db.Role.findOne({ where: { roleCode: ROLES.SUPER_ADMIN } });
if (role) {
await db.UserRole.create({
userId: adminUser.id,
roleId: role.id,
isPrimary: true,
isActive: true
});
}
console.log('✅ Minimal admin seed completed.');
console.log(`✅ Login: ${ADMIN_EMAIL}`);
console.log(`✅ Password: ${ADMIN_PASSWORD}`);
process.exit(0);
} catch (error) {
console.error('❌ Minimal admin seed failed:', error);
process.exit(1);
}
}
seedMinimalAdmin();

View File

@ -0,0 +1,70 @@
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import db from '../src/database/models/index.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function seedStateDistrictOnly() {
console.log('--- Seeding State + District only (no Zone/Region) ---');
try {
await db.sequelize.authenticate();
const seederPath = path.join(__dirname, '../seeders/20240127-seed-geo-data.js');
const content = fs.readFileSync(seederPath, 'utf8');
const statesMatch = content.match(/const STATES_DATA = \[([\s\S]*?)\];/);
const citiesMatch = content.match(/const CITIES_DATA = \[([\s\S]*?)\];/);
if (!statesMatch || !citiesMatch) {
throw new Error('Could not parse STATES_DATA / CITIES_DATA from geo seeder.');
}
// Data source is internal seeder, safe to eval.
const STATES_DATA = eval(`[${statesMatch[1]}]`);
const CITIES_DATA = eval(`[${citiesMatch[1]}]`);
const { State, District } = db;
const stateIdMap = new Map<number, string>();
for (const s of STATES_DATA) {
const [stateRecord] = await State.findOrCreate({
where: { name: s.name },
defaults: {
name: s.name,
zoneId: null
}
});
stateIdMap.set(s.id, stateRecord.id);
}
let districtCount = 0;
for (const c of CITIES_DATA) {
const stateId = stateIdMap.get(c.state_id);
if (!stateId) continue;
await District.findOrCreate({
where: { name: c.name, stateId },
defaults: {
name: c.name,
stateId,
regionId: null,
zoneId: null,
city: c.name,
isActive: true
}
});
districtCount += 1;
}
console.log(`✅ Seeded ${stateIdMap.size} states and ${districtCount} districts.`);
process.exit(0);
} catch (error) {
console.error('❌ State/District seed failed:', error);
process.exit(1);
}
}
seedStateDistrictOnly();

View File

@ -3,6 +3,7 @@ import db from '../src/database/models/index.js';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js'; import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js';
import { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } from '../src/common/config/constants.js'; import { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } from '../src/common/config/constants.js';
import { resolveManagerCode } from '../src/services/userRoleCode.service.js';
const { Role, Zone, Region, State, District, User, UserRole } = db; const { Role, Zone, Region, State, District, User, UserRole } = db;
@ -41,9 +42,17 @@ async function seed() {
const mapUserRole = async (userRec: any, roleCode: string, assignment: any = {}) => { const mapUserRole = async (userRec: any, roleCode: string, assignment: any = {}) => {
const role = await Role.findOne({ where: { roleCode } }); const role = await Role.findOne({ where: { roleCode } });
if (role) { if (role) {
const managerCode = await resolveManagerCode(role.id, roleCode, null);
await UserRole.findOrCreate({ await UserRole.findOrCreate({
where: { userId: userRec.id, roleId: role.id, ...assignment }, where: { userId: userRec.id, roleId: role.id, ...assignment },
defaults: { userId: userRec.id, roleId: role.id, ...assignment, isActive: true, isPrimary: true } defaults: {
userId: userRec.id,
roleId: role.id,
...assignment,
managerCode,
isActive: true,
isPrimary: true
}
}); });
} }
}; };
@ -62,18 +71,21 @@ async function seed() {
// Regions // Regions
const regions = [ const regions = [
{ name: 'NCR Region', zoneName: 'North Zone' }, { name: 'NCR Region', zoneName: 'North Zone', code: 'NZ-R1' },
{ name: 'Punjab Region', zoneName: 'North Zone' }, { name: 'Punjab Region', zoneName: 'North Zone', code: 'NZ-R2' },
{ name: 'Karnataka Region', zoneName: 'South Zone' }, { name: 'Karnataka Region', zoneName: 'South Zone', code: 'SZ-R1' },
{ name: 'Tamil Nadu Region', zoneName: 'South Zone' } { name: 'Tamil Nadu Region', zoneName: 'South Zone', code: 'SZ-R2' }
]; ];
const regionMap: Record<string, any> = {}; const regionMap: Record<string, any> = {};
for (const r of regions) { for (const r of regions) {
const zone = zoneMap[r.zoneName]; const zone = zoneMap[r.zoneName];
const [region] = await Region.findOrCreate({ const [region] = await Region.findOrCreate({
where: { name: r.name }, where: { name: r.name },
defaults: { name: r.name, zoneId: zone.id } defaults: { name: r.name, code: r.code, zoneId: zone.id }
}); });
if (!region.code) {
await region.update({ code: r.code });
}
regionMap[r.name] = region; regionMap[r.name] = region;
} }

View File

@ -0,0 +1,67 @@
import { Op } from 'sequelize';
import { REQUEST_TYPES } from '../config/constants.js';
type DbLike = Record<string, any>;
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 TYPE_ALIASES: Record<string, string> = {
application: 'application',
onboarding: 'application',
resignation: REQUEST_TYPES.RESIGNATION,
relocation: REQUEST_TYPES.RELOCATION,
relocation_request: REQUEST_TYPES.RELOCATION,
constitutional: REQUEST_TYPES.CONSTITUTIONAL,
constitutional_change: REQUEST_TYPES.CONSTITUTIONAL,
'constitutional-change': REQUEST_TYPES.CONSTITUTIONAL,
termination: REQUEST_TYPES.TERMINATION,
fnf: REQUEST_TYPES.FNF
};
const LOOKUP_CONFIG: Record<string, { model: string; codeField: string }> = {
application: { model: 'Application', codeField: 'applicationId' },
resignation: { model: 'Resignation', codeField: 'resignationId' },
relocation: { model: 'RelocationRequest', codeField: 'requestId' },
constitutional: { model: 'ConstitutionalChange', codeField: 'requestId' },
termination: { model: 'TerminationRequest', codeField: 'requestId' },
fnf: { model: 'FnF', codeField: 'settlementId' }
};
export function normalizeRequestType(rawType: string | undefined | null): string {
const type = String(rawType || 'application').trim().toLowerCase();
return TYPE_ALIASES[type] || type;
}
export function requestTypeQueryVariants(normalizedType: string): string[] {
if (normalizedType === REQUEST_TYPES.CONSTITUTIONAL) {
return [REQUEST_TYPES.CONSTITUTIONAL, 'constitutional-change'];
}
return [normalizedType];
}
export async function resolveEntityUuidByType(
db: DbLike,
rawId: string | undefined | null,
rawType: string | undefined | null
): Promise<{ resolvedId: string; normalizedType: string }> {
const id = String(rawId || '').trim();
const normalizedType = normalizeRequestType(rawType);
if (!id) return { resolvedId: id, normalizedType };
const cfg = LOOKUP_CONFIG[normalizedType];
if (!cfg || !db?.[cfg.model]) return { resolvedId: id, normalizedType };
const isUuid = UUID_REGEX.test(id);
const where = isUuid
? { [Op.or]: [{ id }, { [cfg.codeField]: id }] }
: { [cfg.codeField]: id };
const row = await db[cfg.model].findOne({
where,
attributes: ['id']
});
return {
resolvedId: row?.id || id,
normalizedType
};
}

View File

@ -6,6 +6,7 @@ const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
import { AUDIT_ACTIONS } from '../../common/config/constants.js'; import { AUDIT_ACTIONS } from '../../common/config/constants.js';
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../master/syncHierarchy.service.js'; import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../master/syncHierarchy.service.js';
import { resolveManagerCode } from '../../services/userRoleCode.service.js';
const upsertUserAssignments = async ( const upsertUserAssignments = async (
userId: string, userId: string,
@ -23,6 +24,7 @@ const upsertUserAssignments = async (
const role = await Role.findOne({ where: { roleCode } }); const role = await Role.findOne({ where: { roleCode } });
if (!role) continue; if (!role) continue;
const managerCode = await resolveManagerCode(role.id, roleCode, null);
const createdRole = await db.UserRole.create({ const createdRole = await db.UserRole.create({
userId, userId,
@ -30,7 +32,7 @@ const upsertUserAssignments = async (
districtId: assignment.locationId || assignment.districtId || null, districtId: assignment.locationId || assignment.districtId || null,
zoneId: assignment.zoneId || null, zoneId: assignment.zoneId || null,
regionId: assignment.regionId || null, regionId: assignment.regionId || null,
managerCode: assignment.managerCode || assignment.asmCode || null, managerCode,
isPrimary: assignment.isPrimary !== undefined ? Boolean(assignment.isPrimary) : i === 0, isPrimary: assignment.isPrimary !== undefined ? Boolean(assignment.isPrimary) : i === 0,
isActive: assignment.isActive !== undefined ? Boolean(assignment.isActive) : true, isActive: assignment.isActive !== undefined ? Boolean(assignment.isActive) : true,
effectiveFrom: assignment.effectiveFrom || null, effectiveFrom: assignment.effectiveFrom || null,
@ -353,6 +355,13 @@ export const createUser = async (req: AuthRequest, res: Response) => {
// Hash default password // Hash default password
const hashedPassword = await bcrypt.hash('Admin@123', 10); const hashedPassword = await bcrypt.hash('Admin@123', 10);
// Location is optional. Only persist to user.districtId if the value is a valid district UUID.
let safeDistrictId: string | null = null;
if (locationId) {
const districtExists = await db.District.findByPk(locationId);
safeDistrictId = districtExists ? locationId : null;
}
// Create user // Create user
const user = await User.create({ const user = await User.create({
fullName, fullName,
@ -365,7 +374,7 @@ export const createUser = async (req: AuthRequest, res: Response) => {
mobileNumber, mobileNumber,
department, department,
designation, designation,
districtId: locationId districtId: safeDistrictId
}); });
if (Array.isArray(assignments) && assignments.length > 0) { if (Array.isArray(assignments) && assignments.length > 0) {
@ -375,13 +384,14 @@ export const createUser = async (req: AuthRequest, res: Response) => {
if (targetRole) { if (targetRole) {
for (const distId of districts) { for (const distId of districts) {
const sampleDistrict = await db.District.findByPk(distId); const sampleDistrict = await db.District.findByPk(distId);
const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null);
await db.UserRole.create({ await db.UserRole.create({
userId: user.id, userId: user.id,
roleId: targetRole.id, roleId: targetRole.id,
districtId: distId, districtId: distId,
zoneId: sampleDistrict?.zoneId || null, zoneId: sampleDistrict?.zoneId || null,
regionId: sampleDistrict?.regionId || null, regionId: sampleDistrict?.regionId || null,
managerCode: asmCode || null, managerCode,
isPrimary: false, isPrimary: false,
isActive: true, isActive: true,
assignedBy: req.user?.id || null assignedBy: req.user?.id || null
@ -401,12 +411,13 @@ export const createUser = async (req: AuthRequest, res: Response) => {
for (const regId of finalRegionIds) { for (const regId of finalRegionIds) {
const region = await db.Region.findByPk(regId); const region = await db.Region.findByPk(regId);
const managerCode = await resolveManagerCode(targetRole.id, roleCode, null);
await db.UserRole.create({ await db.UserRole.create({
userId: user.id, userId: user.id,
roleId: targetRole.id, roleId: targetRole.id,
regionId: regId, regionId: regId,
zoneId: region?.zoneId || null, zoneId: region?.zoneId || null,
managerCode: zmCode || null, managerCode,
isActive: true, isActive: true,
assignedBy: req.user?.id || null assignedBy: req.user?.id || null
}); });
@ -419,10 +430,12 @@ export const createUser = async (req: AuthRequest, res: Response) => {
} else if (roleCode) { } else if (roleCode) {
const role = await Role.findOne({ where: { roleCode } }); const role = await Role.findOne({ where: { roleCode } });
if (role) { if (role) {
const managerCode = await resolveManagerCode(role.id, roleCode, null);
await db.UserRole.create({ await db.UserRole.create({
userId: user.id, userId: user.id,
roleId: role.id, roleId: role.id,
locationId: locationId || null, districtId: safeDistrictId,
managerCode,
isPrimary: true, isPrimary: true,
isActive: true, isActive: true,
assignedBy: req.user?.id || null assignedBy: req.user?.id || null
@ -542,13 +555,14 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
for (const distId of districts) { for (const distId of districts) {
const sampleDistrict = await db.District.findByPk(distId); const sampleDistrict = await db.District.findByPk(distId);
const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null);
await db.UserRole.create({ await db.UserRole.create({
userId: id, userId: id,
roleId: targetRole.id, roleId: targetRole.id,
districtId: distId, districtId: distId,
zoneId: sampleDistrict?.zoneId || null, zoneId: sampleDistrict?.zoneId || null,
regionId: sampleDistrict?.regionId || null, regionId: sampleDistrict?.regionId || null,
managerCode: asmCode || null, managerCode,
isActive: true, isActive: true,
assignedBy: req.user?.id || null assignedBy: req.user?.id || null
}); });
@ -569,12 +583,13 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
for (const regId of finalRegionIds) { for (const regId of finalRegionIds) {
const region = await db.Region.findByPk(regId); const region = await db.Region.findByPk(regId);
const managerCode = await resolveManagerCode(targetRole.id, roleCode, null);
await db.UserRole.create({ await db.UserRole.create({
userId: id, userId: id,
roleId: targetRole.id, roleId: targetRole.id,
regionId: regId, regionId: regId,
zoneId: region?.zoneId || null, zoneId: region?.zoneId || null,
managerCode: zmCode || null, managerCode,
isActive: true, isActive: true,
assignedBy: req.user?.id || null assignedBy: req.user?.id || null
}); });

View File

@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { AuditLog, User } = db; const { AuditLog, User } = db;
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import { Op } from 'sequelize'; import { resolveEntityUuidByType, normalizeRequestType } from '../../common/utils/requestResolver.js';
// Human-readable descriptions for audit actions // Human-readable descriptions for audit actions
const ACTION_DESCRIPTIONS: Record<string, string> = { const ACTION_DESCRIPTIONS: Record<string, string> = {
@ -216,16 +216,12 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
// Dynamic Table Switching based on Module // Dynamic Table Switching based on Module
// Case-insensitive entity type routing // Case-insensitive entity type routing
const type = (entityType as string).toLowerCase(); const type = normalizeRequestType(entityType as string);
const { resolvedId } = await resolveEntityUuidByType(db as any, entityId as string, type);
if (type === 'resignation') { if (type === 'resignation') {
const resignation = await db.Resignation.findOne({
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
attributes: ['id']
});
const resolvedResignationId = resignation?.id || (entityId as string);
const result = await db.ResignationAudit.findAndCountAll({ const result = await db.ResignationAudit.findAndCountAll({
where: { resignationId: resolvedResignationId }, where: { resignationId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -233,13 +229,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
count = result.count; count = result.count;
logs = result.rows; logs = result.rows;
} else if (type === 'termination') { } else if (type === 'termination') {
const termination = await db.TerminationRequest.findOne({
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedTerminationId = termination?.id || (entityId as string);
const result = await db.TerminationAudit.findAndCountAll({ const result = await db.TerminationAudit.findAndCountAll({
where: { terminationRequestId: resolvedTerminationId }, where: { terminationRequestId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -247,27 +238,17 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
count = result.count; count = result.count;
logs = result.rows; logs = result.rows;
} else if (type === 'fnf') { } else if (type === 'fnf') {
const fnf = await db.FnF.findOne({
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
attributes: ['id']
});
const resolvedFnfId = fnf?.id || (entityId as string);
const result = await db.FnFAudit.findAndCountAll({ const result = await db.FnFAudit.findAndCountAll({
where: { fnfId: resolvedFnfId }, where: { fnfId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
}); });
count = result.count; count = result.count;
logs = result.rows; logs = result.rows;
} else if (type === 'constitutional_change') { } else if (type === 'constitutional') {
const constitutional = await db.ConstitutionalChange.findOne({
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
const result = await db.ConstitutionalAudit.findAndCountAll({ const result = await db.ConstitutionalAudit.findAndCountAll({
where: { constitutionalChangeId: resolvedConstitutionalId }, where: { constitutionalChangeId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -275,13 +256,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
count = result.count; count = result.count;
logs = result.rows; logs = result.rows;
} else if (type === 'relocation') { } else if (type === 'relocation') {
const relocation = await db.RelocationRequest.findOne({
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedRelocationId = relocation?.id || (entityId as string);
const result = await db.RelocationAudit.findAndCountAll({ const result = await db.RelocationAudit.findAndCountAll({
where: { relocationRequestId: resolvedRelocationId }, where: { relocationRequestId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -291,7 +267,7 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
} else { } else {
console.log(`[AuditController] Falling back to global AuditLog for type: ${type}`); console.log(`[AuditController] Falling back to global AuditLog for type: ${type}`);
const result = await db.AuditLog.findAndCountAll({ const result = await db.AuditLog.findAndCountAll({
where: { entityType: entityType as string, entityId: entityId as string }, where: { entityType: entityType as string, entityId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -343,73 +319,49 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
let totalLogs = 0; let totalLogs = 0;
let latestLog: any = null; let latestLog: any = null;
const type = (entityType as string).toLowerCase(); const type = normalizeRequestType(entityType as string);
const { resolvedId } = await resolveEntityUuidByType(db as any, entityId as string, type);
// Dynamic Table Switching // Dynamic Table Switching
if (type === 'resignation') { if (type === 'resignation') {
const resignation = await db.Resignation.findOne({ totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedId } });
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
attributes: ['id']
});
const resolvedResignationId = resignation?.id || (entityId as string);
totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedResignationId } });
latestLog = await db.ResignationAudit.findOne({ latestLog = await db.ResignationAudit.findOne({
where: { resignationId: resolvedResignationId }, where: { resignationId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else if (type === 'termination') { } else if (type === 'termination') {
const termination = await db.TerminationRequest.findOne({ totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedId } });
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedTerminationId = termination?.id || (entityId as string);
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedTerminationId } });
latestLog = await db.TerminationAudit.findOne({ latestLog = await db.TerminationAudit.findOne({
where: { terminationRequestId: resolvedTerminationId }, where: { terminationRequestId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else if (type === 'fnf') { } else if (type === 'fnf') {
const fnf = await db.FnF.findOne({ totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedId } });
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
attributes: ['id']
});
const resolvedFnfId = fnf?.id || (entityId as string);
totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedFnfId } });
latestLog = await db.FnFAudit.findOne({ latestLog = await db.FnFAudit.findOne({
where: { fnfId: resolvedFnfId }, where: { fnfId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else if (type === 'constitutional' || type === 'constitutional_change') { } else if (type === 'constitutional') {
const constitutional = await db.ConstitutionalChange.findOne({ totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedId } });
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedConstitutionalId } });
latestLog = await db.ConstitutionalAudit.findOne({ latestLog = await db.ConstitutionalAudit.findOne({
where: { constitutionalChangeId: resolvedConstitutionalId }, where: { constitutionalChangeId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else if (type === 'relocation' || type === 'relocation_request') { } else if (type === 'relocation') {
const relocation = await db.RelocationRequest.findOne({ totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedId } });
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedRelocationId = relocation?.id || (entityId as string);
totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedRelocationId } });
latestLog = await db.RelocationAudit.findOne({ latestLog = await db.RelocationAudit.findOne({
where: { relocationRequestId: resolvedRelocationId }, where: { relocationRequestId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else { } else {
totalLogs = await db.AuditLog.count({ where: { entityType: entityType as string, entityId: entityId as string } }); totalLogs = await db.AuditLog.count({ where: { entityType: entityType as string, entityId: resolvedId } });
latestLog = await db.AuditLog.findOne({ latestLog = await db.AuditLog.findOne({
where: { entityType: entityType as string, entityId: entityId as string }, where: { entityType: entityType as string, entityId: resolvedId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });

View File

@ -1,5 +1,4 @@
import { Response } from 'express'; import { Response } from 'express';
import { Op } from 'sequelize';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { const {
Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog, Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog,
@ -7,6 +6,7 @@ const {
} = db; } = db;
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import { AUDIT_ACTIONS, REQUEST_TYPES } from '../../common/config/constants.js'; import { AUDIT_ACTIONS, REQUEST_TYPES } from '../../common/config/constants.js';
import { resolveEntityUuidByType, requestTypeQueryVariants } from '../../common/utils/requestResolver.js';
import * as EmailService from '../../common/utils/email.service.js'; import * as EmailService from '../../common/utils/email.service.js';
import { getIO } from '../../common/utils/socket.js'; import { getIO } from '../../common/utils/socket.js';
import * as NotificationService from '../../common/utils/notification.service.js'; import * as NotificationService from '../../common/utils/notification.service.js';
@ -66,38 +66,12 @@ const stitchWorknoteAttachments = async (worknotes: any[]) => {
return Promise.all(notePromises); return Promise.all(notePromises);
}; };
/** Resolve REQ-… vs UUID and align constitutional aliases with `REQUEST_TYPES.CONSTITUTIONAL`. */ function worknoteListWhere(rawId: string, resolvedId: string, normalizedType: string) {
async function resolveWorknoteRequestKeys(rawId: string, rawType: string) { const idVariants = Array.from(new Set([String(rawId || '').trim(), String(resolvedId || '').trim()].filter(Boolean)));
const id = String(rawId || ''); const variants = requestTypeQueryVariants(normalizedType);
let t = String(rawType || 'application').toLowerCase(); const requestIdWhere = idVariants.length > 1 ? { [db.Sequelize.Op.in]: idVariants } : idVariants[0];
if (t === 'constitutional_change') t = 'constitutional-change'; if (variants.length > 1) return { requestId: requestIdWhere, requestType: { [db.Sequelize.Op.in]: variants } };
let resolvedId = id; return { requestId: requestIdWhere, requestType: variants[0] };
if (id && (t === 'constitutional' || t === 'constitutional-change')) {
const row = await db.ConstitutionalChange.findOne({
where: { [Op.or]: [{ id }, { requestId: id }] },
attributes: ['id']
});
if (row) resolvedId = (row as any).id;
t = REQUEST_TYPES.CONSTITUTIONAL;
} else if (id && t === 'relocation') {
const row = await db.RelocationRequest.findOne({
where: { [Op.or]: [{ id }, { requestId: id }] },
attributes: ['id']
});
if (row) resolvedId = (row as any).id;
}
return { resolvedId, normalizedType: t };
}
function worknoteListWhere(resolvedId: string, normalizedType: string) {
if (normalizedType === REQUEST_TYPES.CONSTITUTIONAL) {
return {
requestId: resolvedId,
requestType: { [Op.in]: [REQUEST_TYPES.CONSTITUTIONAL, 'constitutional-change'] }
};
}
return { requestId: resolvedId, requestType: normalizedType };
} }
// --- Worknotes --- // --- Worknotes ---
@ -105,7 +79,7 @@ function worknoteListWhere(resolvedId: string, normalizedType: string) {
export const addWorknote = async (req: AuthRequest, res: Response) => { export const addWorknote = async (req: AuthRequest, res: Response) => {
try { try {
const { requestId, requestType, noteText, noteType, tags, attachmentDocIds } = req.body; const { requestId, requestType, noteText, noteType, tags, attachmentDocIds } = req.body;
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType); const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
logger.info(`Adding worknote for ${normalizedType} ${resolvedId}. Body:`, { noteText, tags, attachmentDocIds }); logger.info(`Adding worknote for ${normalizedType} ${resolvedId}. Body:`, { noteText, tags, attachmentDocIds });
// Debug: Log participants // Debug: Log participants
@ -240,8 +214,8 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
export const getWorknotes = async (req: AuthRequest, res: Response) => { export const getWorknotes = async (req: AuthRequest, res: Response) => {
try { try {
const { requestId, requestType } = req.query as any; const { requestId, requestType } = req.query as any;
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType); const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
const where = worknoteListWhere(resolvedId, normalizedType); const where = worknoteListWhere(String(requestId || ''), resolvedId, normalizedType);
const worknotes = await Worknote.findAll({ const worknotes = await Worknote.findAll({
where, where,
@ -264,7 +238,7 @@ export const uploadWorknoteAttachment = async (req: any, res: Response) => {
try { try {
const file = req.file; const file = req.file;
const { requestId, requestType } = req.body; const { requestId, requestType } = req.body;
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType); const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
if (!file) { if (!file) {
return res.status(400).json({ success: false, message: 'No file uploaded' }); return res.status(400).json({ success: false, message: 'No file uploaded' });

View File

@ -3,8 +3,50 @@ import { Op } from 'sequelize';
import { syncDistrictsByRegion, syncDistrictsByZone, syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js'; import { syncDistrictsByRegion, syncDistrictsByZone, syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
import { ROLES } from '../../common/config/constants.js'; import { ROLES } from '../../common/config/constants.js';
import { resolveManagerCode } from '../../services/userRoleCode.service.js';
const { User } = db; const { User } = db;
const deriveZonePrefix = (zone: any): string => {
const rawCode = String(zone?.code || '').trim().toUpperCase();
if (/^[A-Z]{2,4}$/.test(rawCode)) return rawCode;
const words = String(zone?.name || '')
.trim()
.toUpperCase()
.split(/[\s_-]+/)
.filter(Boolean);
if (words.length >= 2) return `${words[0][0]}${words[1][0]}`;
if (words.length === 1) return words[0].slice(0, 2) || 'RG';
return 'RG';
};
const nextRegionCode = async (zoneId: string, excludeRegionId?: string): Promise<string> => {
const zone = await db.Zone.findByPk(zoneId, { attributes: ['id', 'name', 'code'] });
if (!zone) throw new Error('Zone not found');
const prefix = deriveZonePrefix(zone);
const where: any = {
zoneId,
code: { [Op.iLike]: `${prefix}-R%` }
};
if (excludeRegionId) where.id = { [Op.ne]: excludeRegionId };
const existing = await db.Region.findAll({
where,
attributes: ['code']
});
let maxSeq = 0;
for (const row of existing as any[]) {
const match = String(row.code || '').match(new RegExp(`^${prefix}-R(\\d+)$`, 'i'));
if (!match) continue;
const seq = Number(match[1]);
if (Number.isFinite(seq) && seq > maxSeq) maxSeq = seq;
}
return `${prefix}-R${maxSeq + 1}`;
};
// --- Areas (Granular Locations) --- // --- Areas (Granular Locations) ---
export const getAreas = async (req: Request, res: Response) => { export const getAreas = async (req: Request, res: Response) => {
try { try {
@ -135,7 +177,41 @@ export const getDistricts = async (req: Request, res: Response) => {
export const createDistrict = async (req: Request, res: Response) => { export const createDistrict = async (req: Request, res: Response) => {
try { try {
const { name, code, stateName, city, openFrom, openTo, status, isActive, description } = req.body; const { name, code, stateName, city, openFrom, openTo, status, isActive, description, districtId } = req.body;
// Preferred path: create location against an existing district
if (districtId) {
const district = await db.District.findByPk(districtId);
if (!district) {
return res.status(404).json({ success: false, message: 'District not found' });
}
const areaName = name || city || district.name;
const area = await db.Location.create({
name: areaName,
districtId: district.id,
city: city || areaName,
isActive: isActive !== undefined ? isActive : true,
openFrom: openFrom || null,
openTo: openTo || null,
description: description || null
});
await db.Opportunity.create({
districtId: district.id,
areaId: area.id,
city: city || areaName,
openFrom: openFrom || null,
openTo: openTo || null,
status: status || 'inactive',
opportunityType: 'New Dealership',
capacity: 'Standard',
priority: 'Medium'
});
return res.status(201).json({ success: true, data: area });
}
if (!name) return res.status(400).json({ success: false, message: 'District name is required' }); if (!name) return res.status(400).json({ success: false, message: 'District name is required' });
// Find or Create state if stateName provided // Find or Create state if stateName provided
@ -255,22 +331,30 @@ export const getRegions = async (req: Request, res: Response) => {
export const createRegion = async (req: Request, res: Response) => { export const createRegion = async (req: Request, res: Response) => {
try { try {
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body; const { name, parentId, zoneId, managerId, districts, districtIds } = req.body;
const targetZoneId = zoneId || parentId; const targetZoneId = zoneId || parentId;
if (!name) return res.status(400).json({ success: false, message: 'Region name is required' }); if (!name) return res.status(400).json({ success: false, message: 'Region name is required' });
if (!targetZoneId) return res.status(400).json({ success: false, message: 'Zone is required for region creation' });
const region = await db.Region.create({ name, code, zoneId: targetZoneId }); const generatedCode = await nextRegionCode(targetZoneId);
const region = await db.Region.create({ name, code: generatedCode, zoneId: targetZoneId });
// 1. Assign Manager // 1. Assign Manager (RBM is the regional manager role; keep RM as legacy fallback)
if (managerId) { if (managerId) {
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } }); const rmRole = await db.Role.findOne({
where: {
roleCode: { [db.Sequelize.Op.in]: [ROLES.RBM, 'RM'] }
}
});
if (rmRole) { if (rmRole) {
await db.UserRole.update({ isActive: false }, { where: { regionId: region.id, roleId: rmRole.id } }); await db.UserRole.update({ isActive: false }, { where: { regionId: region.id, roleId: rmRole.id } });
const managerCode = await resolveManagerCode(rmRole.id, rmRole.roleCode, null);
await db.UserRole.create({ await db.UserRole.create({
userId: managerId, userId: managerId,
roleId: rmRole.id, roleId: rmRole.id,
regionId: region.id, regionId: region.id,
zoneId: targetZoneId, zoneId: targetZoneId,
managerCode,
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });
@ -321,32 +405,45 @@ export const createRegion = async (req: Request, res: Response) => {
export const updateRegion = async (req: Request, res: Response) => { export const updateRegion = async (req: Request, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body; const { name, parentId, zoneId, managerId, districts, districtIds } = req.body;
const targetZoneId = zoneId || parentId; const targetZoneId = zoneId || parentId;
const region = await db.Region.findByPk(id); const region = await db.Region.findByPk(id);
if (!region) return res.status(404).json({ success: false, message: 'Region not found' }); if (!region) return res.status(404).json({ success: false, message: 'Region not found' });
const nextZoneId = targetZoneId || region.zoneId;
const zoneChanged = Boolean(targetZoneId && targetZoneId !== region.zoneId);
const generatedCode =
zoneChanged || !region.code
? await nextRegionCode(nextZoneId, id as string)
: region.code;
await region.update({ await region.update({
name, name,
code, code: generatedCode,
zoneId: targetZoneId || region.zoneId zoneId: nextZoneId
}); });
// 1. Update Manager // 1. Update Manager (RBM is the regional manager role; keep RM as legacy fallback)
if (managerId) { if (managerId) {
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } }); const rmRole = await db.Role.findOne({
where: {
roleCode: { [db.Sequelize.Op.in]: [ROLES.RBM, 'RM'] }
}
});
if (rmRole) { if (rmRole) {
// Deactivate old RMs for this region // Deactivate old RMs for this region
await db.UserRole.update({ isActive: false }, { await db.UserRole.update({ isActive: false }, {
where: { regionId: id, roleId: rmRole.id } where: { regionId: id, roleId: rmRole.id }
}); });
// Assign new RM // Assign new RM
const managerCode = await resolveManagerCode(rmRole.id, rmRole.roleCode, null);
await db.UserRole.create({ await db.UserRole.create({
userId: managerId, userId: managerId,
roleId: rmRole.id, roleId: rmRole.id,
regionId: id, regionId: id,
zoneId: region.zoneId, zoneId: region.zoneId,
managerCode,
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });
@ -703,17 +800,25 @@ export const deleteLocation = async (req: Request, res: Response) => {
export const updateLocation = async (req: Request, res: Response) => { export const updateLocation = async (req: Request, res: Response) => {
try { try {
const { id } = req.params; // This is the Area ID const { id } = req.params; // This is the Area ID
const { name, code, stateName, city, openFrom, openTo, status, isActive, description } = req.body; const { name, code, stateName, city, openFrom, openTo, status, isActive, description, districtId } = req.body;
const area = await db.Location.findByPk(id, { const area = await db.Location.findByPk(id, {
include: [{ model: db.District, as: 'district' }] include: [{ model: db.District, as: 'district' }]
}); });
if (!area) return res.status(404).json({ success: false, message: 'Area not found' }); if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
const district = area.district; let district = area.district;
if (districtId && (!district || String(district.id) !== String(districtId))) {
const nextDistrict = await db.District.findByPk(districtId);
if (!nextDistrict) {
return res.status(404).json({ success: false, message: 'District not found' });
}
district = nextDistrict;
}
// 1. Update District // 1. Update District
if (district) { if (district && !districtId) {
let stateId = req.body.stateId; let stateId = req.body.stateId;
if (stateName && !stateId) { if (stateName && !stateId) {
const [state] = await db.State.findOrCreate({ const [state] = await db.State.findOrCreate({
@ -734,6 +839,7 @@ export const updateLocation = async (req: Request, res: Response) => {
// 2. Update Area // 2. Update Area
await area.update({ await area.update({
name: name || area.name, name: name || area.name,
districtId: district?.id || area.districtId,
city: city || area.city, city: city || area.city,
isActive: isActive !== undefined ? isActive : area.isActive, isActive: isActive !== undefined ? isActive : area.isActive,
openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom, openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom,
@ -860,7 +966,7 @@ export const getZonalManagers = async (req: Request, res: Response) => {
{ {
model: db.Role, model: db.Role,
as: 'role', as: 'role',
where: { roleCode: { [db.Sequelize.Op.in]: ['ZM', 'DD-ZM', 'ZBH'] } } where: { roleCode: { [db.Sequelize.Op.in]: ['ZM', 'DD-ZM'] } }
}, },
{ {
model: db.Region, model: db.Region,
@ -879,7 +985,7 @@ export const getZonalManagers = async (req: Request, res: Response) => {
}); });
const result = (zms || []).map((u: any) => { const result = (zms || []).map((u: any) => {
const rolePriority = ['DD-ZM', 'ZM', 'ZBH']; const rolePriority = ['DD-ZM', 'ZM'];
const roleAssignments = (u.userRoles || []).filter((r: any) => rolePriority.includes(r.role?.roleCode)); const roleAssignments = (u.userRoles || []).filter((r: any) => rolePriority.includes(r.role?.roleCode));
const mainAssignment = [...roleAssignments].sort((a: any, b: any) => { const mainAssignment = [...roleAssignments].sort((a: any, b: any) => {
@ -958,7 +1064,7 @@ export const saveZM = async (req: Request, res: Response) => {
roleId: zmRole.id, roleId: zmRole.id,
zoneId: zoneId || null, zoneId: zoneId || null,
regionId: regionId, regionId: regionId,
managerCode: zmCode || null, managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null),
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });
@ -970,7 +1076,7 @@ export const saveZM = async (req: Request, res: Response) => {
roleId: zmRole.id, roleId: zmRole.id,
zoneId: zoneId || null, zoneId: zoneId || null,
regionId: null, regionId: null,
managerCode: zmCode || null, managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null),
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });
@ -1074,7 +1180,7 @@ export const saveDDLead = async (req: Request, res: Response) => {
userId, userId,
roleId: leadRole.id, roleId: leadRole.id,
zoneId: zoneId, zoneId: zoneId,
managerCode: leadCode || null, managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null),
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });
@ -1085,7 +1191,7 @@ export const saveDDLead = async (req: Request, res: Response) => {
userId, userId,
roleId: leadRole.id, roleId: leadRole.id,
zoneId: null, zoneId: null,
managerCode: leadCode || null, managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null),
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });

View File

@ -123,6 +123,13 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
stage: application.currentStage stage: application.currentStage
}); });
// Ensure district/region/zone manager pointers are fresh, then auto-map evaluators
// so Application Details shows RBM/ZBH (and other mapped roles) immediately.
if (districtId) {
await syncLocationManagers(districtId);
await assignStageEvaluators(application.id);
}
// Send Email (Async) // Send Email (Async)
if (isOpportunityAvailable) { if (isOpportunityAvailable) {
sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId) sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId)

View File

@ -13,6 +13,7 @@ import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWork
import { NomenclatureService } from '../../common/utils/nomenclature.js'; import { NomenclatureService } from '../../common/utils/nomenclature.js';
import { ParticipantService } from '../../services/ParticipantService.js'; import { ParticipantService } from '../../services/ParticipantService.js';
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js'; import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
import { import {
isRegisteredConstitutionalChangeType, isRegisteredConstitutionalChangeType,
normalizeToConstitutionalChangeType normalizeToConstitutionalChangeType
@ -22,6 +23,11 @@ const STRUCTURE_TARGET_VALUES = new Set<string>(
CONSTITUTIONAL_STRUCTURE_TARGET_OPTIONS.map((o) => o.value as string) CONSTITUTIONAL_STRUCTURE_TARGET_OPTIONS.map((o) => o.value as string)
); );
const resolveConstitutionalUuid = async (id: string) => {
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'constitutional');
return resolvedId;
};
export const getMeta = async (_req: AuthRequest, res: Response) => { export const getMeta = async (_req: AuthRequest, res: Response) => {
try { try {
res.json({ res.json({
@ -261,11 +267,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
export const getRequestById = async (req: AuthRequest, res: Response) => { export const getRequestById = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const idStr = String(id); const resolvedId = await resolveConstitutionalUuid(String(id));
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
const request = await ConstitutionalChange.findOne({ const request = await ConstitutionalChange.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }, where: { id: resolvedId },
include: [ include: [
{ model: Outlet, as: 'outlet' }, { model: Outlet, as: 'outlet' },
{ {
@ -370,14 +375,13 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
const { id } = req.params; const { id } = req.params;
const idStr = String(id);
const rawAction = String(req.body.action || '').trim(); const rawAction = String(req.body.action || '').trim();
const actionNorm = rawAction.toLowerCase().replace(/\s+/g, ' '); const actionNorm = rawAction.toLowerCase().replace(/\s+/g, ' ');
const comments = req.body.comments; const comments = req.body.comments;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr); const resolvedId = await resolveConstitutionalUuid(String(id));
const request = await ConstitutionalChange.findOne({ const request = await ConstitutionalChange.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } where: { id: resolvedId }
}); });
if (!request) return res.status(404).json({ success: false, message: 'Request not found' }); if (!request) return res.status(404).json({ success: false, message: 'Request not found' });
@ -523,12 +527,11 @@ export const uploadDocuments = async (req: AuthRequest, res: Response) => {
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
const { id } = req.params; const { id } = req.params;
const idStr = String(id);
const { documents } = req.body; const { documents } = req.body;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr); const resolvedId = await resolveConstitutionalUuid(String(id));
const request = await ConstitutionalChange.findOne({ const request = await ConstitutionalChange.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } where: { id: resolvedId }
}); });
if (!request) { if (!request) {

View File

@ -8,6 +8,12 @@ import { Op, Transaction } from 'sequelize';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import { formatDateTime } from '../../common/utils/dateUtils.js'; import { formatDateTime } from '../../common/utils/dateUtils.js';
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
const resolveRelocationUuid = async (id: string) => {
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'relocation');
return resolvedId;
};
/** /**
* Helper to assign evaluators for relocation requests based on outlet location hierarchy * Helper to assign evaluators for relocation requests based on outlet location hierarchy
@ -386,12 +392,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
export const getRequestById = async (req: AuthRequest, res: Response) => { export const getRequestById = async (req: AuthRequest, res: Response) => {
try { try {
const id = req.params.id as string; const id = req.params.id as string;
const resolvedId = await resolveRelocationUuid(id);
// Check if id is a UUID or a requestId string
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id);
const request = await RelocationRequest.findOne({ const request = await RelocationRequest.findOne({
where: isUUID ? { id } : { requestId: id }, where: { id: resolvedId },
include: [ include: [
{ {
model: Outlet, model: Outlet,
@ -515,12 +519,10 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
.toUpperCase() .toUpperCase()
.replace(/\s+/g, '_'); .replace(/\s+/g, '_');
// Check if id is a UUID or a requestId string const resolvedId = await resolveRelocationUuid(String(id));
const idStr = String(id);
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
const request = await RelocationRequest.findOne({ const request = await RelocationRequest.findOne({
where: isUUID ? { id: idStr } : { requestId: idStr } where: { id: resolvedId }
}); });
if (!request) { if (!request) {
@ -720,12 +722,10 @@ export const uploadDocuments = async (req: AuthRequest, res: Response) => {
return res.status(400).json({ success: false, message: 'Document type is required' }); return res.status(400).json({ success: false, message: 'Document type is required' });
} }
const idStr = String(id); const resolvedId = await resolveRelocationUuid(String(id));
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
// Only search by requestId since frontend sends requestId, not UUID
const request = await RelocationRequest.findOne({ const request = await RelocationRequest.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } where: { id: resolvedId }
}); });
if (!request) { if (!request) {
@ -806,11 +806,10 @@ const applyRelocationDocumentDecision = async (
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
const idStr = String(id); const resolvedId = await resolveRelocationUuid(String(id));
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
const request = await RelocationRequest.findOne({ const request = await RelocationRequest.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } where: { id: resolvedId }
}); });
if (!request) { if (!request) {
return res.status(404).json({ success: false, message: 'Relocation request not found' }); return res.status(404).json({ success: false, message: 'Relocation request not found' });

View File

@ -6,6 +6,7 @@ import {
AUDIT_ACTIONS, AUDIT_ACTIONS,
ROLES, ROLES,
REQUEST_TYPES, REQUEST_TYPES,
FNF_STATUS,
RESIGNATION_DOCUMENT_TYPES, RESIGNATION_DOCUMENT_TYPES,
RESIGNATION_DOCUMENT_STAGES RESIGNATION_DOCUMENT_STAGES
} from '../../common/config/constants.js'; } from '../../common/config/constants.js';
@ -18,8 +19,13 @@ import { NomenclatureService } from '../../common/utils/nomenclature.js';
import { ParticipantService } from '../../services/ParticipantService.js'; import { ParticipantService } from '../../services/ParticipantService.js';
import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js'; import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js'; import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
// Removed generateResignationId and moved to NomenclatureService // Removed generateResignationId and moved to NomenclatureService
const resolveResignationUuid = async (id: string) => {
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'resignation');
return resolvedId;
};
// Create resignation request (Dealer only) // Create resignation request (Dealer only)
export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
@ -132,11 +138,10 @@ export const getResignations = async (req: AuthRequest, res: Response, next: Nex
export const getResignationById = async (req: AuthRequest, res: Response, next: NextFunction) => { export const getResignationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
try { try {
const { id } = req.params; const { id } = req.params;
const idStr = String(id); const resolvedId = await resolveResignationUuid(String(id));
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, where: { id: resolvedId },
include: [ include: [
{ model: db.Outlet, as: 'outlet' }, { model: db.Outlet, as: 'outlet' },
{ {
@ -218,10 +223,9 @@ export const uploadResignationDocument = async (req: AuthRequest, res: Response,
message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}` message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}`
}); });
} }
const idStr = String(id); const resolvedId = await resolveResignationUuid(String(id));
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } where: { id: resolvedId }
}); });
if (!resignation) { if (!resignation) {
@ -275,12 +279,11 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
try { try {
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const idStr = String(id);
const { remarks } = req.body; const { remarks } = req.body;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr); const resolvedId = await resolveResignationUuid(String(id));
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, where: { id: resolvedId },
include: [ include: [
{ model: db.Outlet, as: 'outlet' }, { model: db.Outlet, as: 'outlet' },
{ model: db.User, as: 'dealer', attributes: ['id', 'dealerId'] } { model: db.User, as: 'dealer', attributes: ['id', 'dealerId'] }
@ -317,6 +320,21 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
return res.status(400).json({ success: false, message: 'Cannot move to next stage from current state' }); return res.status(400).json({ success: false, message: 'Cannot move to next stage from current state' });
} }
// Sequence guard: resignation can be marked completed only after F&F settlement is complete.
if (
resignation.currentStage === RESIGNATION_STAGES.FNF_INITIATED &&
nextStage === RESIGNATION_STAGES.COMPLETED
) {
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id }, transaction });
if (!fnf || fnf.status !== FNF_STATUS.COMPLETED) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Cannot complete resignation. F&F settlement must be completed first.'
});
}
}
const sourceStage = resignation.currentStage; const sourceStage = resignation.currentStage;
// Transition via Workflow Service // Transition via Workflow Service
@ -405,16 +423,15 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
try { try {
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const idStr = String(id);
const { reason } = req.body; const { reason } = req.body;
if (!reason) { if (!reason) {
await transaction.rollback(); await transaction.rollback();
return res.status(400).json({ success: false, message: 'Rejection reason is required' }); return res.status(400).json({ success: false, message: 'Rejection reason is required' });
} }
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr); const resolvedId = await resolveResignationUuid(String(id));
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, where: { id: resolvedId },
include: [{ model: db.Outlet, as: 'outlet' }] include: [{ model: db.Outlet, as: 'outlet' }]
}); });
if (!resignation) { if (!resignation) {
@ -457,12 +474,11 @@ export const withdrawResignation = async (req: AuthRequest, res: Response, next:
try { try {
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const idStr = String(id);
const { reason } = req.body; const { reason } = req.body;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr); const resolvedId = await resolveResignationUuid(String(id));
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, where: { id: resolvedId },
include: [{ model: db.Outlet, as: 'outlet' }] include: [{ model: db.Outlet, as: 'outlet' }]
}); });
if (!resignation) { if (!resignation) {
@ -524,12 +540,11 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next:
try { try {
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const idStr = String(id);
const { targetStage, remarks } = req.body; const { targetStage, remarks } = req.body;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr); const resolvedId = await resolveResignationUuid(String(id));
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } where: { id: resolvedId }
}); });
if (!resignation) { if (!resignation) {
await transaction.rollback(); await transaction.rollback();
@ -590,8 +605,9 @@ export const assignResignation = async (req: AuthRequest, res: Response, next: N
const { id } = req.params; const { id } = req.params;
const { assignTo, remarks } = req.body; // assignTo is a role code or specific userId const { assignTo, remarks } = req.body; // assignTo is a role code or specific userId
const resolvedId = await resolveResignationUuid(String(id));
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: { [Op.or]: [{ id }, { resignationId: id }] }, where: { id: resolvedId },
include: [{ model: db.User, as: 'dealer' }] include: [{ model: db.User, as: 'dealer' }]
}); });
@ -687,12 +703,11 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
try { try {
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const idStr = String(id);
const { department, status, remarks, amount, type } = req.body; const { department, status, remarks, amount, type } = req.body;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr); const resolvedId = await resolveResignationUuid(String(id));
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } where: { id: resolvedId }
}); });
if (!resignation) { if (!resignation) {
await transaction.rollback(); await transaction.rollback();
@ -850,6 +865,35 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
if (!req.user || !authorizedRoles.includes(req.user.roleCode as any)) { if (!req.user || !authorizedRoles.includes(req.user.roleCode as any)) {
return res.status(403).json({ success: false, message: 'You do not have permission to push this request to F&F' }); return res.status(403).json({ success: false, message: 'You do not have permission to push this request to F&F' });
} }
{
const resolvedId = await resolveResignationUuid(String(req.params.id));
const resignation = await db.Resignation.findByPk(resolvedId);
if (!resignation) {
return res.status(404).json({ success: false, message: 'Resignation not found' });
}
// SRS-aligned gate: F&F can start only after Legal completion artifacts.
if (resignation.currentStage !== RESIGNATION_STAGES.LEGAL) {
return res.status(400).json({
success: false,
message: `Cannot trigger F&F from ${resignation.currentStage}. Move request to Legal stage first.`
});
}
const hasLegalStageDocument = await db.ResignationDocument.findOne({
where: {
resignationId: resignation.id,
stage: RESIGNATION_STAGES.LEGAL
},
attributes: ['id']
});
if (!hasLegalStageDocument) {
return res.status(400).json({
success: false,
message: 'Cannot trigger F&F. Legal-stage acceptance/communication document is required first.'
});
}
}
// Jump directly to F&F Initiation // Jump directly to F&F Initiation
(req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED; (req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED;
return approveResignation(req, res, next); return approveResignation(req, res, next);

View File

@ -8,7 +8,7 @@ import {
TERMINATION_DOCUMENT_TYPES, TERMINATION_DOCUMENT_TYPES,
TERMINATION_DOCUMENT_STAGES TERMINATION_DOCUMENT_STAGES
} from '../../common/config/constants.js'; } from '../../common/config/constants.js';
import { Op, Transaction } from 'sequelize'; import { Transaction } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js'; import ExternalMocksService from '../../common/utils/externalMocks.service.js';
@ -17,6 +17,12 @@ import { NomenclatureService } from '../../common/utils/nomenclature.js';
import { ParticipantService } from '../../services/ParticipantService.js'; import { ParticipantService } from '../../services/ParticipantService.js';
import { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js'; import { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js'; import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
const resolveTerminationUuid = async (id: string) => {
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'termination');
return resolvedId;
};
// Create termination request // Create termination request
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => { export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
@ -102,11 +108,10 @@ export const getTerminations = async (req: AuthRequest, res: Response, next: Nex
export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => { export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
try { try {
const { id } = req.params; const { id } = req.params;
const idStr = String(id); const resolvedId = await resolveTerminationUuid(String(id));
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
const termination = await db.TerminationRequest.findOne({ const termination = await db.TerminationRequest.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }, where: { id: resolvedId },
include: [ include: [
{ {
model: db.Dealer, model: db.Dealer,
@ -184,10 +189,9 @@ export const uploadTerminationDocument = async (req: AuthRequest, res: Response,
message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}` message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}`
}); });
} }
const idStr = String(id); const resolvedId = await resolveTerminationUuid(String(id));
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
const termination = await db.TerminationRequest.findOne({ const termination = await db.TerminationRequest.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } where: { id: resolvedId }
}); });
if (!termination) { if (!termination) {
@ -240,8 +244,9 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const { action, remarks } = req.body; const { action, remarks } = req.body;
const resolvedId = await resolveTerminationUuid(String(id));
const termination = await db.TerminationRequest.findByPk(id); const termination = await db.TerminationRequest.findByPk(resolvedId);
if (!termination) { if (!termination) {
await transaction.rollback(); await transaction.rollback();
return res.status(404).json({ success: false, message: 'Termination not found' }); return res.status(404).json({ success: false, message: 'Termination not found' });
@ -394,8 +399,9 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const { department, status, amount, type, remarks } = req.body; const { department, status, amount, type, remarks } = req.body;
const resolvedId = await resolveTerminationUuid(String(id));
const termination = await db.TerminationRequest.findByPk(id); const termination = await db.TerminationRequest.findByPk(resolvedId);
if (!termination) throw new Error('Termination request not found'); if (!termination) throw new Error('Termination request not found');
const clearances = { ...(termination.departmentalClearances || {}) }; const clearances = { ...(termination.departmentalClearances || {}) };
@ -412,7 +418,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
await termination.update({ departmentalClearances: clearances }, { transaction }); await termination.update({ departmentalClearances: clearances }, { transaction });
// Update individual clearance record for unified dashboard // Update individual clearance record for unified dashboard
const fnf = await db.FnF.findOne({ where: { terminationRequestId: id } }); const fnf = await db.FnF.findOne({ where: { terminationRequestId: resolvedId } });
if (fnf) { if (fnf) {
await db.FffClearance.update( await db.FffClearance.update(
{ status: normalizedStatus, remarks, amount: Number(amount) || 0 }, { status: normalizedStatus, remarks, amount: Number(amount) || 0 },
@ -423,7 +429,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
await db.TerminationAudit.create({ await db.TerminationAudit.create({
userId: req.user.id, userId: req.user.id,
action: 'CLEARANCE_UPDATED', action: 'CLEARANCE_UPDATED',
terminationRequestId: id, terminationRequestId: resolvedId,
remarks: remarks || `Cleared ${department}`, remarks: remarks || `Cleared ${department}`,
details: { department, status: normalizedStatus, amount } details: { department, status: normalizedStatus, amount }
}, { transaction }); }, { transaction });

View File

@ -127,7 +127,8 @@ export class ResignationWorkflowService {
[RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD, [RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD,
[RESIGNATION_STAGES.NBH]: ROLES.NBH, [RESIGNATION_STAGES.NBH]: ROLES.NBH,
[RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN, [RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN,
[RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN [RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN,
[RESIGNATION_STAGES.FNF_INITIATED]: ROLES.DD_ADMIN
}; };
const requiredRole = stageToRole[resignation.currentStage]; const requiredRole = stageToRole[resignation.currentStage];

View File

@ -0,0 +1,52 @@
import { Op } from 'sequelize';
import db from '../database/models/index.js';
const ROLE_CODE_PREFIX: Record<string, string> = {
ASM: 'ASM',
RBM: 'RBM',
RM: 'RBM',
'DD-ZM': 'ZM',
ZM: 'ZM',
ZBH: 'ZBH',
'DD Lead': 'DDL'
};
const normalizeCode = (value: string) =>
value
.trim()
.toUpperCase()
.replace(/\s+/g, '-');
const shouldGenerateCode = (roleCode?: string | null) => {
if (!roleCode) return false;
return Boolean(ROLE_CODE_PREFIX[roleCode]);
};
export const resolveManagerCode = async (
roleId: string,
roleCode?: string | null,
providedCode?: string | null
): Promise<string | null> => {
const normalizedProvided = providedCode ? normalizeCode(providedCode) : null;
if (normalizedProvided) return normalizedProvided;
if (!shouldGenerateCode(roleCode)) return null;
const prefix = ROLE_CODE_PREFIX[roleCode as string];
const rows = await db.UserRole.findAll({
where: {
roleId,
managerCode: { [Op.iLike]: `${prefix}-%` }
},
attributes: ['managerCode']
});
let maxSeq = 0;
for (const row of rows as any[]) {
const code = String(row.managerCode || '');
const parts = code.split('-');
const seq = Number(parts[parts.length - 1]);
if (Number.isFinite(seq) && seq > maxSeq) maxSeq = seq;
}
return `${prefix}-${String(maxSeq + 1).padStart(3, '0')}`;
};

View File

@ -20,16 +20,16 @@ const PROSPECT_EMAIL = `ramesh_${timestamp}@gmail.com`;
const EMAILS = { const EMAILS = {
PROSPECT: PROSPECT_EMAIL, PROSPECT: PROSPECT_EMAIL,
RBM_L1: 'rbm.ncr@royalenfield.com', RBM_L1: 'manish@gmail.com',
ZM_L1: 'zm.ncr@royalenfield.com', ZM_L1: 'piyush@gmail.com',
DD_LEAD: 'ddlead@royalenfield.com', DD_LEAD: 'jaya@gmail.com',
ZBH: 'yashwin@gmail.com', ZBH: 'manav@gmail.com',
NBH: 'nbh@royalenfield.com', NBH: 'yashwin@gmail.com',
DD_HEAD: 'ddhead@royalenfield.com', DD_HEAD: 'ganesh@gmail.com',
FDD: 'fdd@royalenfield.com', FDD: 'fdd@gmail.com',
FINANCE: 'finance@royalenfield.com', FINANCE: 'finance@gmail.com',
DD_ADMIN: 'lince@gmail.com', DD_ADMIN: 'aman@gmail.com',
ASM: 'asm.sdelhi@royalenfield.com', ASM: 'abhishek@gmail.com',
SALES: 'sales@royalenfield.com', SALES: 'sales@royalenfield.com',
SERVICE: 'service@royalenfield.com', SERVICE: 'service@royalenfield.com',
SPARES: 'spares@royalenfield.com', SPARES: 'spares@royalenfield.com',
@ -123,7 +123,7 @@ async function prospectLogin(phone) {
async function mockUploadDocument(appId, token, docType) { async function mockUploadDocument(appId, token, docType) {
const formData = new FormData(); const formData = new FormData();
const fileBuffer = fs.readFileSync('C:/Users/BACKPACKERS/Pictures/claim_document_type.PNG'); const fileBuffer = fs.readFileSync('/home/laxman-h/Pictures/Screenshots/Screenshot from 2026-03-26 10-08-00.png');
const blob = new Blob([fileBuffer], { type: 'image/png' }); const blob = new Blob([fileBuffer], { type: 'image/png' });
formData.append('file', blob, 'screenshot.png'); formData.append('file', blob, 'screenshot.png');
formData.append('documentType', docType); formData.append('documentType', docType);
@ -347,48 +347,48 @@ async function triggerWorkflow() {
await delay(); await delay();
// 6.3 FDD ASSIGNMENT // 6.3 FDD ASSIGNMENT
log(6.3, 'Admin Assigning Application to FDD Agency...'); // log(6.3, 'Admin Assigning Application to FDD Agency...');
const fddUser = users.data.find(u => u.email === EMAILS.FDD); // const fddUser = users.data.find(u => u.email === EMAILS.FDD);
await apiRequest('/fdd/assign', 'POST', { // await apiRequest('/fdd/assign', 'POST', {
applicationId: applicationUUID, // applicationId: applicationUUID,
assignedToAgency: fddUser.id // assignedToAgency: fddUser.id
}, adminToken); // }, adminToken);
log(6.3, 'FDD Agency assigned successfully.'); // log(6.3, 'FDD Agency assigned successfully.');
await delay(); // await delay();
// 7. FDD MILESTONE // // 7. FDD MILESTONE
log(7, 'FDD Agency Discovery & Report Upload...'); // log(7, 'FDD Agency Discovery & Report Upload...');
const fddToken = await login(EMAILS.FDD); // const fddToken = await login(EMAILS.FDD);
// FETCH ASSIGNMENT ID // // FETCH ASSIGNMENT ID
const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken); // const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
const assignmentId = assignmentRes.data.id; // const assignmentId = assignmentRes.data.id;
log(7, `Found Assignment ID: ${assignmentId}`); // log(7, `Found Assignment ID: ${assignmentId}`);
await apiRequest('/fdd/report', 'POST', { // await apiRequest('/fdd/report', 'POST', {
assignmentId, // assignmentId,
findings: 'Finance records clean.', // findings: 'Finance records clean.',
recommendation: 'Approved' // recommendation: 'Approved'
}, fddToken); // }, fddToken);
log(7.1, 'Admin Approving FDD Final Stage...'); // log(7.1, 'Admin Approving FDD Final Stage...');
await apiRequest('/assessment/stage-decision', 'POST', { // await apiRequest('/assessment/stage-decision', 'POST', {
applicationId: applicationUUID, // applicationId: applicationUUID,
stageCode: 'FDD_VERIFICATION', // stageCode: 'FDD_VERIFICATION',
decision: 'Approved', // decision: 'Approved',
remarks: 'FDD documents verified.' // remarks: 'FDD documents verified.'
}, adminToken); // }, adminToken);
log(7, 'FDD Milestone Complete.'); // log(7, 'FDD Milestone Complete.');
await delay(); // await delay();
log(7.4, 'Uploading mandatory documents prior to LOI generation...'); // log(7.4, 'Uploading mandatory documents prior to LOI generation...');
const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card']; // const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
for (const doc of requiredDocs) { // for (const doc of requiredDocs) {
await mockUploadDocument(applicationUUID, adminToken, doc); // await mockUploadDocument(applicationUUID, adminToken, doc);
} // }
await delay(1000); // await delay(1000);
// 7.5 LOI APPROVAL // // 7.5 LOI APPROVAL
// log(7.5, 'LOI Generation & Approval...'); // log(7.5, 'LOI Generation & Approval...');
// const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken); // const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
// const loiRequestId = loiRes.data.id; // const loiRequestId = loiRes.data.id;
@ -418,10 +418,8 @@ async function triggerWorkflow() {
// depositType: 'SECURITY_DEPOSIT', // depositType: 'SECURITY_DEPOSIT',
// status: 'Verified' // status: 'Verified'
// }, financeToken); // }, financeToken);
// log(8, 'Security Deposit Verified.'); // log(8, 'Security Deposit Verified.')
// await delay(); // // 9. GENERATE DEALER CODES (align with backend gate: LOI Issued required)
// // 9. GENERATE DEALER CODES (NOW RETRY-SAFE WITH STATUS CHECK)
// let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken); // let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
// log(9, `Current status before code generation: ${statusBeforeCodeGen}`); // log(9, `Current status before code generation: ${statusBeforeCodeGen}`);
// log(9, 'Ensuring mandatory PAN/GST/Bank fields before code generation...'); // log(9, 'Ensuring mandatory PAN/GST/Bank fields before code generation...');
@ -442,6 +440,23 @@ async function triggerWorkflow() {
// log(9, `Status after re-verify: ${statusBeforeCodeGen}`); // log(9, `Status after re-verify: ${statusBeforeCodeGen}`);
// } // }
// // Current backend flow keeps app at "Security Details" until explicit admin transition.
// if (statusBeforeCodeGen === 'Security Details') {
// log(9, 'Applying admin transition from Security Details -> LOI Issued...');
// await apiRequest(`/onboarding/applications/${applicationUUID}/status`, 'PUT', {
// status: 'LOI Issued',
// stage: 'LOI',
// reason: 'E2E script alignment: unlock dealer code generation after Security Details checks.'
// }, adminToken);
// await delay();
// statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
// log(9, `Status after admin transition: ${statusBeforeCodeGen}`);
// }
// if (statusBeforeCodeGen !== 'LOI Issued' && statusBeforeCodeGen !== 'Dealer Code Generation') {
// throw new Error(`Cannot generate codes: expected LOI Issued/Dealer Code Generation, got ${statusBeforeCodeGen}`);
// }
// log(9, 'Admin Generating SAP Dealer Codes...'); // log(9, 'Admin Generating SAP Dealer Codes...');
// await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken); // await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
// log(9, 'Dealer Codes Generated.'); // log(9, 'Dealer Codes Generated.');