diff --git a/package.json b/package.json index 5967ff9..181477f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "migrate": "tsx scripts/migrate.ts", "reset:stable": "tsx scripts/reset_db_stable.ts", "seed": "tsx scripts/seed_normalized_data.ts", + "seed:roles": "tsx scripts/seed-roles.ts", "seed:permissions": "tsx scripts/seed-permissions.ts", "seed:approval-policies": "tsx scripts/seed-approval-policies.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", "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: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", "seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts", "test": "jest", diff --git a/scripts/reset_db_stable.ts b/scripts/reset_db_stable.ts index 0146a5b..ad7fbb9 100644 --- a/scripts/reset_db_stable.ts +++ b/scripts/reset_db_stable.ts @@ -2,6 +2,7 @@ import 'dotenv/config'; import db from '../src/database/models/index.js'; import bcrypt from 'bcryptjs'; 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; @@ -84,6 +85,7 @@ async function masterReset() { // Map assignments based on role category (Regional vs Granular) const isRegionalRole = [ROLES.RBM, ROLES.DD_ZM, ROLES.ZBH].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({ userId: user.id, @@ -92,7 +94,8 @@ async function masterReset() { isPrimary: true, zoneId: isRegionalRole || isGranularRole ? zone.id : null, regionId: isRegionalRole ? region.id : null, - districtId: isGranularRole ? district.id : null + districtId: isGranularRole ? district.id : null, + managerCode }); } } diff --git a/scripts/seed-minimal-admin.ts b/scripts/seed-minimal-admin.ts new file mode 100644 index 0000000..6f38be6 --- /dev/null +++ b/scripts/seed-minimal-admin.ts @@ -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(); diff --git a/scripts/seed-state-district-only.ts b/scripts/seed-state-district-only.ts new file mode 100644 index 0000000..ff85c27 --- /dev/null +++ b/scripts/seed-state-district-only.ts @@ -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(); + + 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(); diff --git a/scripts/seed_normalized_data.ts b/scripts/seed_normalized_data.ts index 9108d4d..045e5d7 100644 --- a/scripts/seed_normalized_data.ts +++ b/scripts/seed_normalized_data.ts @@ -3,6 +3,7 @@ import db from '../src/database/models/index.js'; import bcrypt from 'bcryptjs'; 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 { resolveManagerCode } from '../src/services/userRoleCode.service.js'; 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 role = await Role.findOne({ where: { roleCode } }); if (role) { + const managerCode = await resolveManagerCode(role.id, roleCode, null); await UserRole.findOrCreate({ 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 const regions = [ - { name: 'NCR Region', zoneName: 'North Zone' }, - { name: 'Punjab Region', zoneName: 'North Zone' }, - { name: 'Karnataka Region', zoneName: 'South Zone' }, - { name: 'Tamil Nadu Region', zoneName: 'South Zone' } + { name: 'NCR Region', zoneName: 'North Zone', code: 'NZ-R1' }, + { name: 'Punjab Region', zoneName: 'North Zone', code: 'NZ-R2' }, + { name: 'Karnataka Region', zoneName: 'South Zone', code: 'SZ-R1' }, + { name: 'Tamil Nadu Region', zoneName: 'South Zone', code: 'SZ-R2' } ]; const regionMap: Record = {}; for (const r of regions) { const zone = zoneMap[r.zoneName]; const [region] = await Region.findOrCreate({ 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; } diff --git a/src/common/utils/requestResolver.ts b/src/common/utils/requestResolver.ts new file mode 100644 index 0000000..6686710 --- /dev/null +++ b/src/common/utils/requestResolver.ts @@ -0,0 +1,67 @@ +import { Op } from 'sequelize'; +import { REQUEST_TYPES } from '../config/constants.js'; + +type DbLike = Record; +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 = { + 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 = { + 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 + }; +} diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index 4e99199..6821d13 100644 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -6,6 +6,7 @@ const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db; import { AUDIT_ACTIONS } from '../../common/config/constants.js'; import { AuthRequest } from '../../types/express.types.js'; import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../master/syncHierarchy.service.js'; +import { resolveManagerCode } from '../../services/userRoleCode.service.js'; const upsertUserAssignments = async ( userId: string, @@ -23,6 +24,7 @@ const upsertUserAssignments = async ( const role = await Role.findOne({ where: { roleCode } }); if (!role) continue; + const managerCode = await resolveManagerCode(role.id, roleCode, null); const createdRole = await db.UserRole.create({ userId, @@ -30,7 +32,7 @@ const upsertUserAssignments = async ( districtId: assignment.locationId || assignment.districtId || null, zoneId: assignment.zoneId || null, regionId: assignment.regionId || null, - managerCode: assignment.managerCode || assignment.asmCode || null, + managerCode, isPrimary: assignment.isPrimary !== undefined ? Boolean(assignment.isPrimary) : i === 0, isActive: assignment.isActive !== undefined ? Boolean(assignment.isActive) : true, effectiveFrom: assignment.effectiveFrom || null, @@ -353,6 +355,13 @@ export const createUser = async (req: AuthRequest, res: Response) => { // Hash default password 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 const user = await User.create({ fullName, @@ -365,7 +374,7 @@ export const createUser = async (req: AuthRequest, res: Response) => { mobileNumber, department, designation, - districtId: locationId + districtId: safeDistrictId }); if (Array.isArray(assignments) && assignments.length > 0) { @@ -375,13 +384,14 @@ export const createUser = async (req: AuthRequest, res: Response) => { if (targetRole) { for (const distId of districts) { const sampleDistrict = await db.District.findByPk(distId); + const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null); await db.UserRole.create({ userId: user.id, roleId: targetRole.id, districtId: distId, zoneId: sampleDistrict?.zoneId || null, regionId: sampleDistrict?.regionId || null, - managerCode: asmCode || null, + managerCode, isPrimary: false, isActive: true, assignedBy: req.user?.id || null @@ -401,12 +411,13 @@ export const createUser = async (req: AuthRequest, res: Response) => { for (const regId of finalRegionIds) { const region = await db.Region.findByPk(regId); + const managerCode = await resolveManagerCode(targetRole.id, roleCode, null); await db.UserRole.create({ userId: user.id, roleId: targetRole.id, regionId: regId, zoneId: region?.zoneId || null, - managerCode: zmCode || null, + managerCode, isActive: true, assignedBy: req.user?.id || null }); @@ -419,10 +430,12 @@ export const createUser = async (req: AuthRequest, res: Response) => { } else if (roleCode) { const role = await Role.findOne({ where: { roleCode } }); if (role) { + const managerCode = await resolveManagerCode(role.id, roleCode, null); await db.UserRole.create({ userId: user.id, roleId: role.id, - locationId: locationId || null, + districtId: safeDistrictId, + managerCode, isPrimary: true, isActive: true, assignedBy: req.user?.id || null @@ -542,13 +555,14 @@ export const updateUser = async (req: AuthRequest, res: Response) => { for (const distId of districts) { const sampleDistrict = await db.District.findByPk(distId); + const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null); await db.UserRole.create({ userId: id, roleId: targetRole.id, districtId: distId, zoneId: sampleDistrict?.zoneId || null, regionId: sampleDistrict?.regionId || null, - managerCode: asmCode || null, + managerCode, isActive: true, assignedBy: req.user?.id || null }); @@ -569,12 +583,13 @@ export const updateUser = async (req: AuthRequest, res: Response) => { for (const regId of finalRegionIds) { const region = await db.Region.findByPk(regId); + const managerCode = await resolveManagerCode(targetRole.id, roleCode, null); await db.UserRole.create({ userId: id, roleId: targetRole.id, regionId: regId, zoneId: region?.zoneId || null, - managerCode: zmCode || null, + managerCode, isActive: true, assignedBy: req.user?.id || null }); diff --git a/src/modules/audit/audit.controller.ts b/src/modules/audit/audit.controller.ts index 4ab17fb..6008b6e 100644 --- a/src/modules/audit/audit.controller.ts +++ b/src/modules/audit/audit.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; const { AuditLog, User } = db; 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 const ACTION_DESCRIPTIONS: Record = { @@ -216,16 +216,12 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => { // Dynamic Table Switching based on Module // 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') { - 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({ - where: { resignationId: resolvedResignationId }, + where: { resignationId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], order: [['createdAt', 'DESC']], limit: limitNum, offset @@ -233,13 +229,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => { count = result.count; logs = result.rows; } 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({ - where: { terminationRequestId: resolvedTerminationId }, + where: { terminationRequestId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], order: [['createdAt', 'DESC']], limit: limitNum, offset @@ -247,27 +238,17 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => { count = result.count; logs = result.rows; } 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({ - where: { fnfId: resolvedFnfId }, + where: { fnfId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], order: [['createdAt', 'DESC']], limit: limitNum, offset }); count = result.count; logs = result.rows; - } else if (type === 'constitutional_change') { - 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); + } else if (type === 'constitutional') { const result = await db.ConstitutionalAudit.findAndCountAll({ - where: { constitutionalChangeId: resolvedConstitutionalId }, + where: { constitutionalChangeId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], order: [['createdAt', 'DESC']], limit: limitNum, offset @@ -275,13 +256,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => { count = result.count; logs = result.rows; } 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({ - where: { relocationRequestId: resolvedRelocationId }, + where: { relocationRequestId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], order: [['createdAt', 'DESC']], limit: limitNum, offset @@ -291,7 +267,7 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => { } else { console.log(`[AuditController] Falling back to global AuditLog for type: ${type}`); 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'] }], order: [['createdAt', 'DESC']], limit: limitNum, offset @@ -343,73 +319,49 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => { let totalLogs = 0; 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 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); - totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedResignationId } }); + totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedId } }); latestLog = await db.ResignationAudit.findOne({ - where: { resignationId: resolvedResignationId }, + where: { resignationId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], order: [['createdAt', 'DESC']] }); } 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); - totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedTerminationId } }); + totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedId } }); latestLog = await db.TerminationAudit.findOne({ - where: { terminationRequestId: resolvedTerminationId }, + where: { terminationRequestId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], order: [['createdAt', 'DESC']] }); } 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); - totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedFnfId } }); + totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedId } }); latestLog = await db.FnFAudit.findOne({ - where: { fnfId: resolvedFnfId }, + where: { fnfId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], order: [['createdAt', 'DESC']] }); - } else if (type === 'constitutional' || type === 'constitutional_change') { - 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); - totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedConstitutionalId } }); + } else if (type === 'constitutional') { + totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedId } }); latestLog = await db.ConstitutionalAudit.findOne({ - where: { constitutionalChangeId: resolvedConstitutionalId }, + where: { constitutionalChangeId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], order: [['createdAt', 'DESC']] }); - } else if (type === 'relocation' || type === 'relocation_request') { - 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); - totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedRelocationId } }); + } else if (type === 'relocation') { + totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedId } }); latestLog = await db.RelocationAudit.findOne({ - where: { relocationRequestId: resolvedRelocationId }, + where: { relocationRequestId: resolvedId }, include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], order: [['createdAt', 'DESC']] }); } 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({ - 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'] }], order: [['createdAt', 'DESC']] }); diff --git a/src/modules/collaboration/collaboration.controller.ts b/src/modules/collaboration/collaboration.controller.ts index 463756e..6ee47ea 100644 --- a/src/modules/collaboration/collaboration.controller.ts +++ b/src/modules/collaboration/collaboration.controller.ts @@ -1,5 +1,4 @@ import { Response } from 'express'; -import { Op } from 'sequelize'; import db from '../../database/models/index.js'; const { Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog, @@ -7,6 +6,7 @@ const { } = db; import { AuthRequest } from '../../types/express.types.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 { getIO } from '../../common/utils/socket.js'; import * as NotificationService from '../../common/utils/notification.service.js'; @@ -66,38 +66,12 @@ const stitchWorknoteAttachments = async (worknotes: any[]) => { return Promise.all(notePromises); }; -/** Resolve REQ-… vs UUID and align constitutional aliases with `REQUEST_TYPES.CONSTITUTIONAL`. */ -async function resolveWorknoteRequestKeys(rawId: string, rawType: string) { - const id = String(rawId || ''); - let t = String(rawType || 'application').toLowerCase(); - if (t === 'constitutional_change') t = 'constitutional-change'; - let resolvedId = id; - - 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 }; +function worknoteListWhere(rawId: string, resolvedId: string, normalizedType: string) { + const idVariants = Array.from(new Set([String(rawId || '').trim(), String(resolvedId || '').trim()].filter(Boolean))); + const variants = requestTypeQueryVariants(normalizedType); + const requestIdWhere = idVariants.length > 1 ? { [db.Sequelize.Op.in]: idVariants } : idVariants[0]; + if (variants.length > 1) return { requestId: requestIdWhere, requestType: { [db.Sequelize.Op.in]: variants } }; + return { requestId: requestIdWhere, requestType: variants[0] }; } // --- Worknotes --- @@ -105,7 +79,7 @@ function worknoteListWhere(resolvedId: string, normalizedType: string) { export const addWorknote = async (req: AuthRequest, res: Response) => { try { 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 }); // Debug: Log participants @@ -240,8 +214,8 @@ export const addWorknote = async (req: AuthRequest, res: Response) => { export const getWorknotes = async (req: AuthRequest, res: Response) => { try { const { requestId, requestType } = req.query as any; - const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType); - const where = worknoteListWhere(resolvedId, normalizedType); + const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType); + const where = worknoteListWhere(String(requestId || ''), resolvedId, normalizedType); const worknotes = await Worknote.findAll({ where, @@ -264,7 +238,7 @@ export const uploadWorknoteAttachment = async (req: any, res: Response) => { try { const file = req.file; const { requestId, requestType } = req.body; - const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType); + const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType); if (!file) { return res.status(400).json({ success: false, message: 'No file uploaded' }); diff --git a/src/modules/master/master.controller.ts b/src/modules/master/master.controller.ts index 33120f1..ee86dba 100644 --- a/src/modules/master/master.controller.ts +++ b/src/modules/master/master.controller.ts @@ -3,8 +3,50 @@ import { Op } from 'sequelize'; import { syncDistrictsByRegion, syncDistrictsByZone, syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js'; import db from '../../database/models/index.js'; import { ROLES } from '../../common/config/constants.js'; +import { resolveManagerCode } from '../../services/userRoleCode.service.js'; 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 => { + 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) --- export const getAreas = async (req: Request, res: Response) => { try { @@ -135,7 +177,41 @@ export const getDistricts = async (req: Request, res: Response) => { export const createDistrict = async (req: Request, res: Response) => { 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' }); // 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) => { try { - const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body; + const { name, parentId, zoneId, managerId, districts, districtIds } = req.body; const targetZoneId = zoneId || parentId; 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 generatedCode = await nextRegionCode(targetZoneId); + const region = await db.Region.create({ name, code: generatedCode, zoneId: targetZoneId }); - const region = await db.Region.create({ name, code, zoneId: targetZoneId }); - - // 1. Assign Manager + // 1. Assign Manager (RBM is the regional manager role; keep RM as legacy fallback) 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) { 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({ userId: managerId, roleId: rmRole.id, regionId: region.id, zoneId: targetZoneId, + managerCode, isActive: true, isPrimary: true }); @@ -321,32 +405,45 @@ export const createRegion = async (req: Request, res: Response) => { export const updateRegion = async (req: Request, res: Response) => { try { 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 region = await db.Region.findByPk(id); if (!region) return res.status(404).json({ success: false, message: 'Region not found' }); - await region.update({ - name, - code, - zoneId: targetZoneId || region.zoneId + 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({ + name, + code: generatedCode, + zoneId: nextZoneId }); - // 1. Update Manager + // 1. Update Manager (RBM is the regional manager role; keep RM as legacy fallback) 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) { // Deactivate old RMs for this region await db.UserRole.update({ isActive: false }, { where: { regionId: id, roleId: rmRole.id } }); // Assign new RM + const managerCode = await resolveManagerCode(rmRole.id, rmRole.roleCode, null); await db.UserRole.create({ userId: managerId, roleId: rmRole.id, regionId: id, zoneId: region.zoneId, + managerCode, isActive: true, isPrimary: true }); @@ -703,17 +800,25 @@ export const deleteLocation = async (req: Request, res: Response) => { export const updateLocation = async (req: Request, res: Response) => { try { 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, { include: [{ model: db.District, as: 'district' }] }); 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 - if (district) { + if (district && !districtId) { let stateId = req.body.stateId; if (stateName && !stateId) { const [state] = await db.State.findOrCreate({ @@ -734,6 +839,7 @@ export const updateLocation = async (req: Request, res: Response) => { // 2. Update Area await area.update({ name: name || area.name, + districtId: district?.id || area.districtId, city: city || area.city, isActive: isActive !== undefined ? isActive : area.isActive, openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom, @@ -860,7 +966,7 @@ export const getZonalManagers = async (req: Request, res: Response) => { { model: db.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, @@ -879,7 +985,7 @@ export const getZonalManagers = async (req: Request, res: Response) => { }); 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 mainAssignment = [...roleAssignments].sort((a: any, b: any) => { @@ -958,7 +1064,7 @@ export const saveZM = async (req: Request, res: Response) => { roleId: zmRole.id, zoneId: zoneId || null, regionId: regionId, - managerCode: zmCode || null, + managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null), isActive: true, isPrimary: true }); @@ -970,7 +1076,7 @@ export const saveZM = async (req: Request, res: Response) => { roleId: zmRole.id, zoneId: zoneId || null, regionId: null, - managerCode: zmCode || null, + managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null), isActive: true, isPrimary: true }); @@ -1074,7 +1180,7 @@ export const saveDDLead = async (req: Request, res: Response) => { userId, roleId: leadRole.id, zoneId: zoneId, - managerCode: leadCode || null, + managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null), isActive: true, isPrimary: true }); @@ -1085,7 +1191,7 @@ export const saveDDLead = async (req: Request, res: Response) => { userId, roleId: leadRole.id, zoneId: null, - managerCode: leadCode || null, + managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null), isActive: true, isPrimary: true }); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index b961256..d3722a3 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -123,6 +123,13 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { 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) if (isOpportunityAvailable) { sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId) diff --git a/src/modules/self-service/constitutional.controller.ts b/src/modules/self-service/constitutional.controller.ts index b5af21f..d88e105 100644 --- a/src/modules/self-service/constitutional.controller.ts +++ b/src/modules/self-service/constitutional.controller.ts @@ -13,6 +13,7 @@ import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWork import { NomenclatureService } from '../../common/utils/nomenclature.js'; import { ParticipantService } from '../../services/ParticipantService.js'; import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js'; +import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js'; import { isRegisteredConstitutionalChangeType, normalizeToConstitutionalChangeType @@ -22,6 +23,11 @@ const STRUCTURE_TARGET_VALUES = new Set( 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) => { try { res.json({ @@ -261,11 +267,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => { export const getRequestById = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - 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 resolvedId = await resolveConstitutionalUuid(String(id)); const request = await ConstitutionalChange.findOne({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }, + where: { id: resolvedId }, include: [ { 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' }); const { id } = req.params; - const idStr = String(id); const rawAction = String(req.body.action || '').trim(); const actionNorm = rawAction.toLowerCase().replace(/\s+/g, ' '); 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({ - 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' }); @@ -523,12 +527,11 @@ export const uploadDocuments = async (req: AuthRequest, res: Response) => { if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); const { id } = req.params; - const idStr = String(id); 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({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } + where: { id: resolvedId } }); if (!request) { diff --git a/src/modules/self-service/relocation.controller.ts b/src/modules/self-service/relocation.controller.ts index 089cc18..e96364e 100644 --- a/src/modules/self-service/relocation.controller.ts +++ b/src/modules/self-service/relocation.controller.ts @@ -8,6 +8,12 @@ import { Op, Transaction } from 'sequelize'; import { v4 as uuidv4 } from 'uuid'; import { AuthRequest } from '../../types/express.types.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 @@ -386,12 +392,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => { export const getRequestById = async (req: AuthRequest, res: Response) => { try { const id = req.params.id as string; - - // 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 resolvedId = await resolveRelocationUuid(id); const request = await RelocationRequest.findOne({ - where: isUUID ? { id } : { requestId: id }, + where: { id: resolvedId }, include: [ { model: Outlet, @@ -515,12 +519,10 @@ export const takeAction = async (req: AuthRequest, res: Response) => { .toUpperCase() .replace(/\s+/g, '_'); - // Check if id is a UUID or a requestId string - 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 resolvedId = await resolveRelocationUuid(String(id)); const request = await RelocationRequest.findOne({ - where: isUUID ? { id: idStr } : { requestId: idStr } + where: { id: resolvedId } }); 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' }); } - 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 resolvedId = await resolveRelocationUuid(String(id)); - // Only search by requestId since frontend sends requestId, not UUID const request = await RelocationRequest.findOne({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } + where: { id: resolvedId } }); if (!request) { @@ -806,11 +806,10 @@ const applyRelocationDocumentDecision = async ( if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); - 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 resolvedId = await resolveRelocationUuid(String(id)); const request = await RelocationRequest.findOne({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } + where: { id: resolvedId } }); if (!request) { return res.status(404).json({ success: false, message: 'Relocation request not found' }); diff --git a/src/modules/self-service/resignation.controller.ts b/src/modules/self-service/resignation.controller.ts index 4b04192..1b80b4e 100644 --- a/src/modules/self-service/resignation.controller.ts +++ b/src/modules/self-service/resignation.controller.ts @@ -6,6 +6,7 @@ import { AUDIT_ACTIONS, ROLES, REQUEST_TYPES, + FNF_STATUS, RESIGNATION_DOCUMENT_TYPES, RESIGNATION_DOCUMENT_STAGES } from '../../common/config/constants.js'; @@ -18,8 +19,13 @@ import { NomenclatureService } from '../../common/utils/nomenclature.js'; import { ParticipantService } from '../../services/ParticipantService.js'; import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js'; import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js'; +import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js'; // 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) 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) => { try { const { id } = req.params; - 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 resolvedId = await resolveResignationUuid(String(id)); 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' }, { @@ -218,10 +223,9 @@ export const uploadResignationDocument = async (req: AuthRequest, res: Response, message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}` }); } - 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 resolvedId = await resolveResignationUuid(String(id)); const resignation = await db.Resignation.findOne({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } + where: { id: resolvedId } }); if (!resignation) { @@ -275,12 +279,11 @@ export const approveResignation = async (req: AuthRequest, res: Response, next: try { if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; - const idStr = String(id); 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({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, + where: { id: resolvedId }, include: [ { model: db.Outlet, as: 'outlet' }, { 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' }); } + // 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; // Transition via Workflow Service @@ -405,16 +423,15 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N try { if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; - const idStr = String(id); const { reason } = req.body; if (!reason) { await transaction.rollback(); 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({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, + where: { id: resolvedId }, include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { @@ -457,12 +474,11 @@ export const withdrawResignation = async (req: AuthRequest, res: Response, next: try { if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; - const idStr = String(id); 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({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, + where: { id: resolvedId }, include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { @@ -524,12 +540,11 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next: try { if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; - const idStr = String(id); 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({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } + where: { id: resolvedId } }); if (!resignation) { await transaction.rollback(); @@ -590,8 +605,9 @@ export const assignResignation = async (req: AuthRequest, res: Response, next: N const { id } = req.params; 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({ - where: { [Op.or]: [{ id }, { resignationId: id }] }, + where: { id: resolvedId }, include: [{ model: db.User, as: 'dealer' }] }); @@ -687,12 +703,11 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex try { if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; - const idStr = String(id); 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({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } + where: { id: resolvedId } }); if (!resignation) { 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)) { 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 (req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED; return approveResignation(req, res, next); diff --git a/src/modules/termination/termination.controller.ts b/src/modules/termination/termination.controller.ts index 6109f17..20b6c8f 100644 --- a/src/modules/termination/termination.controller.ts +++ b/src/modules/termination/termination.controller.ts @@ -8,7 +8,7 @@ import { TERMINATION_DOCUMENT_TYPES, TERMINATION_DOCUMENT_STAGES } from '../../common/config/constants.js'; -import { Op, Transaction } from 'sequelize'; +import { Transaction } from 'sequelize'; import { AuthRequest } from '../../types/express.types.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 { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.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 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) => { try { const { id } = req.params; - 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 resolvedId = await resolveTerminationUuid(String(id)); const termination = await db.TerminationRequest.findOne({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }, + where: { id: resolvedId }, include: [ { model: db.Dealer, @@ -184,10 +189,9 @@ export const uploadTerminationDocument = async (req: AuthRequest, res: Response, message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}` }); } - 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 resolvedId = await resolveTerminationUuid(String(id)); const termination = await db.TerminationRequest.findOne({ - where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr } + where: { id: resolvedId } }); if (!termination) { @@ -240,8 +244,9 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; 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) { await transaction.rollback(); 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'); const { id } = req.params; 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'); const clearances = { ...(termination.departmentalClearances || {}) }; @@ -412,7 +418,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex await termination.update({ departmentalClearances: clearances }, { transaction }); // 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) { await db.FffClearance.update( { 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({ userId: req.user.id, action: 'CLEARANCE_UPDATED', - terminationRequestId: id, + terminationRequestId: resolvedId, remarks: remarks || `Cleared ${department}`, details: { department, status: normalizedStatus, amount } }, { transaction }); diff --git a/src/services/ResignationWorkflowService.ts b/src/services/ResignationWorkflowService.ts index f25612b..8f90c2f 100644 --- a/src/services/ResignationWorkflowService.ts +++ b/src/services/ResignationWorkflowService.ts @@ -127,7 +127,8 @@ export class ResignationWorkflowService { [RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD, [RESIGNATION_STAGES.NBH]: ROLES.NBH, [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]; diff --git a/src/services/userRoleCode.service.ts b/src/services/userRoleCode.service.ts new file mode 100644 index 0000000..6905ade --- /dev/null +++ b/src/services/userRoleCode.service.ts @@ -0,0 +1,52 @@ +import { Op } from 'sequelize'; +import db from '../database/models/index.js'; + +const ROLE_CODE_PREFIX: Record = { + 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 => { + 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')}`; +}; diff --git a/trigger-workflow.js b/trigger-workflow.js index 190fdfb..7dbac93 100644 --- a/trigger-workflow.js +++ b/trigger-workflow.js @@ -20,16 +20,16 @@ const PROSPECT_EMAIL = `ramesh_${timestamp}@gmail.com`; const EMAILS = { PROSPECT: PROSPECT_EMAIL, - RBM_L1: 'rbm.ncr@royalenfield.com', - ZM_L1: 'zm.ncr@royalenfield.com', - DD_LEAD: 'ddlead@royalenfield.com', - ZBH: 'yashwin@gmail.com', - NBH: 'nbh@royalenfield.com', - DD_HEAD: 'ddhead@royalenfield.com', - FDD: 'fdd@royalenfield.com', - FINANCE: 'finance@royalenfield.com', - DD_ADMIN: 'lince@gmail.com', - ASM: 'asm.sdelhi@royalenfield.com', + RBM_L1: 'manish@gmail.com', + ZM_L1: 'piyush@gmail.com', + DD_LEAD: 'jaya@gmail.com', + ZBH: 'manav@gmail.com', + NBH: 'yashwin@gmail.com', + DD_HEAD: 'ganesh@gmail.com', + FDD: 'fdd@gmail.com', + FINANCE: 'finance@gmail.com', + DD_ADMIN: 'aman@gmail.com', + ASM: 'abhishek@gmail.com', SALES: 'sales@royalenfield.com', SERVICE: 'service@royalenfield.com', SPARES: 'spares@royalenfield.com', @@ -123,7 +123,7 @@ async function prospectLogin(phone) { async function mockUploadDocument(appId, token, docType) { 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' }); formData.append('file', blob, 'screenshot.png'); formData.append('documentType', docType); @@ -347,48 +347,48 @@ async function triggerWorkflow() { await delay(); // 6.3 FDD ASSIGNMENT - log(6.3, 'Admin Assigning Application to FDD Agency...'); - const fddUser = users.data.find(u => u.email === EMAILS.FDD); - await apiRequest('/fdd/assign', 'POST', { - applicationId: applicationUUID, - assignedToAgency: fddUser.id - }, adminToken); - log(6.3, 'FDD Agency assigned successfully.'); - await delay(); + // log(6.3, 'Admin Assigning Application to FDD Agency...'); + // const fddUser = users.data.find(u => u.email === EMAILS.FDD); + // await apiRequest('/fdd/assign', 'POST', { + // applicationId: applicationUUID, + // assignedToAgency: fddUser.id + // }, adminToken); + // log(6.3, 'FDD Agency assigned successfully.'); + // await delay(); - // 7. FDD MILESTONE - log(7, 'FDD Agency Discovery & Report Upload...'); - const fddToken = await login(EMAILS.FDD); + // // 7. FDD MILESTONE + // log(7, 'FDD Agency Discovery & Report Upload...'); + // const fddToken = await login(EMAILS.FDD); - // FETCH ASSIGNMENT ID - const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken); - const assignmentId = assignmentRes.data.id; - log(7, `Found Assignment ID: ${assignmentId}`); + // // FETCH ASSIGNMENT ID + // const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken); + // const assignmentId = assignmentRes.data.id; + // log(7, `Found Assignment ID: ${assignmentId}`); - await apiRequest('/fdd/report', 'POST', { - assignmentId, - findings: 'Finance records clean.', - recommendation: 'Approved' - }, fddToken); + // await apiRequest('/fdd/report', 'POST', { + // assignmentId, + // findings: 'Finance records clean.', + // recommendation: 'Approved' + // }, fddToken); - log(7.1, 'Admin Approving FDD Final Stage...'); - await apiRequest('/assessment/stage-decision', 'POST', { - applicationId: applicationUUID, - stageCode: 'FDD_VERIFICATION', - decision: 'Approved', - remarks: 'FDD documents verified.' - }, adminToken); - log(7, 'FDD Milestone Complete.'); - await delay(); + // log(7.1, 'Admin Approving FDD Final Stage...'); + // await apiRequest('/assessment/stage-decision', 'POST', { + // applicationId: applicationUUID, + // stageCode: 'FDD_VERIFICATION', + // decision: 'Approved', + // remarks: 'FDD documents verified.' + // }, adminToken); + // log(7, 'FDD Milestone Complete.'); + // await delay(); - log(7.4, 'Uploading mandatory documents prior to LOI generation...'); - const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card']; - for (const doc of requiredDocs) { - await mockUploadDocument(applicationUUID, adminToken, doc); - } - await delay(1000); + // log(7.4, 'Uploading mandatory documents prior to LOI generation...'); + // const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card']; + // for (const doc of requiredDocs) { + // await mockUploadDocument(applicationUUID, adminToken, doc); + // } + // await delay(1000); - // 7.5 LOI APPROVAL + // // 7.5 LOI APPROVAL // log(7.5, 'LOI Generation & Approval...'); // const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken); // const loiRequestId = loiRes.data.id; @@ -418,10 +418,8 @@ async function triggerWorkflow() { // depositType: 'SECURITY_DEPOSIT', // status: 'Verified' // }, financeToken); - // log(8, 'Security Deposit Verified.'); - // await delay(); - - // // 9. GENERATE DEALER CODES (NOW RETRY-SAFE WITH STATUS CHECK) + // log(8, 'Security Deposit Verified.') + // // 9. GENERATE DEALER CODES (align with backend gate: LOI Issued required) // let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken); // log(9, `Current status before code generation: ${statusBeforeCodeGen}`); // 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}`); // } + // // 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...'); // await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken); // log(9, 'Dealer Codes Generated.');