diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 494a799..37a552d 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -166,10 +166,15 @@ export const RELOCATION_TYPES = { // Relocation Stages export const RELOCATION_STAGES = { - DD_ADMIN_REVIEW: 'DD Admin Review', + ASM_REVIEW: 'ASM Review', RBM_REVIEW: 'RBM Review', + DD_ZM_REVIEW: 'DD ZM Review', + ZBH_REVIEW: 'ZBH Review', + DD_LEAD_REVIEW: 'DD Lead Review', + DD_HEAD_APPROVAL: 'DD Head Approval', NBH_APPROVAL: 'NBH Approval', LEGAL_CLEARANCE: 'Legal Clearance', + NBH_CLEARANCE_EOR: 'NBH Clearance with EOR', COMPLETED: 'Completed', REJECTED: 'Rejected' } as const; @@ -338,6 +343,18 @@ export const DOCUMENT_TYPES = { SECURITY_DEPOSIT_RECEIPT: 'Security Deposit Receipt', SECURITY_DEPOSIT_INITIAL: 'Initial Security Deposit Receipt', SECURITY_DEPOSIT_FINAL: 'Final Security Deposit Receipt', + RELOCATION_PROPERTY_DOCS: 'Property documents for new location', + RELOCATION_LEASE_AGREEMENT: 'Lease/Rental agreement for new location', + RELOCATION_NOC_LANDLORD: 'NOC from current landlord', + RELOCATION_MUNICIPAL_APPROVALS: 'Municipal approvals', + RELOCATION_FIRE_SAFETY: 'Fire safety certificate', + RELOCATION_POLLUTION_CLEARANCE: 'Pollution clearance', + RELOCATION_LAYOUT_PLAN: 'Layout/Floor plan of new location', + RELOCATION_PHOTOS: 'Photos of new location', + RELOCATION_LOCALITY_MAP: 'Locality map', + RELOCATION_BUILDING_PLAN: 'Building plan approval', + RELOCATION_ELECTRICITY_DOCS: 'Electricity connection documents', + RELOCATION_WATER_DOCS: 'Water supply documents', OTHER: 'Other' } as const; diff --git a/src/common/utils/dateUtils.ts b/src/common/utils/dateUtils.ts new file mode 100644 index 0000000..ab22b62 --- /dev/null +++ b/src/common/utils/dateUtils.ts @@ -0,0 +1,34 @@ +/** + * Common date formatting utility for backend modules + */ +export function formatDateTime(date: string | Date | number, format: 'full' | 'date' | 'time' = 'full') { + const d = new Date(date); + + // Check for invalid date + if (isNaN(d.getTime())) return 'Invalid Date'; + + const options: Intl.DateTimeFormatOptions = { + day: '2-digit', + month: 'short', + year: 'numeric', + }; + + if (format === 'full' || format === 'time') { + options.hour = '2-digit'; + options.minute = '2-digit'; + options.hour12 = true; + } + + if (format === 'time') { + return d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true }); + } + + return d.toLocaleDateString('en-IN', options); +} + +/** + * Returns current timestamp in YYYY-MM-DD HH:mm:ss format + */ +export function getCurrentTimestamp() { + return new Date().toISOString().replace('T', ' ').substring(0, 19); +} diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index f014542..26e9d48 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -248,7 +248,8 @@ export default (sequelize: Sequelize) => { Application.hasMany(models.Document, { foreignKey: 'requestId', as: 'uploadedDocuments', - scope: { requestType: 'application' } + scope: { requestType: 'application' }, + constraints: false }); Application.hasMany(models.QuestionnaireResponse, { foreignKey: 'applicationId', as: 'questionnaireResponses' }); diff --git a/src/database/models/EorChecklist.ts b/src/database/models/EorChecklist.ts index f1868fe..cdda860 100644 --- a/src/database/models/EorChecklist.ts +++ b/src/database/models/EorChecklist.ts @@ -2,7 +2,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize'; export interface EorChecklistAttributes { id: string; - applicationId: string; + applicationId: string | null; + relocationId: string | null; auditorId: string | null; auditDate: Date | null; status: string; @@ -20,12 +21,20 @@ export default (sequelize: Sequelize) => { }, applicationId: { type: DataTypes.UUID, - allowNull: false, + allowNull: true, references: { model: 'applications', key: 'id' } }, + relocationId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'relocation_requests', + key: 'id' + } + }, auditorId: { type: DataTypes.UUID, allowNull: true, @@ -53,6 +62,7 @@ export default (sequelize: Sequelize) => { (EorChecklist as any).associate = (models: any) => { EorChecklist.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' }); + EorChecklist.belongsTo(models.RelocationRequest, { foreignKey: 'relocationId', as: 'relocation' }); EorChecklist.belongsTo(models.User, { foreignKey: 'auditorId', as: 'auditor' }); EorChecklist.hasMany(models.EorChecklistItem, { foreignKey: 'checklistId', as: 'items' }); diff --git a/src/database/models/RelocationRequest.ts b/src/database/models/RelocationRequest.ts index e4b6bc2..9bf0b08 100644 --- a/src/database/models/RelocationRequest.ts +++ b/src/database/models/RelocationRequest.ts @@ -88,7 +88,7 @@ export default (sequelize: Sequelize) => { }, currentStage: { type: DataTypes.ENUM(...Object.values(RELOCATION_STAGES)), - defaultValue: RELOCATION_STAGES.DD_ADMIN_REVIEW + defaultValue: RELOCATION_STAGES.ASM_REVIEW }, status: { type: DataTypes.STRING, @@ -140,6 +140,16 @@ export default (sequelize: Sequelize) => { scope: { requestType: 'relocation' }, constraints: false }); + RelocationRequest.hasMany(models.Document, { + foreignKey: 'requestId', + as: 'uploadedDocuments', + scope: { requestType: 'relocation' }, + constraints: false + }); + RelocationRequest.hasOne(models.EorChecklist, { + foreignKey: 'relocationId', + as: 'eorChecklist' + }); // Note: Participants are computed dynamically based on outlet location hierarchy // See getRequestById in relocation.controller.ts }; diff --git a/src/modules/eor/eor.controller.ts b/src/modules/eor/eor.controller.ts index 7b01ae2..0cadb8b 100644 --- a/src/modules/eor/eor.controller.ts +++ b/src/modules/eor/eor.controller.ts @@ -5,9 +5,9 @@ import { AuthRequest } from '../../types/express.types.js'; export const getChecklist = async (req: Request, res: Response) => { try { - const { applicationId } = req.params; + const { applicationId, relocationId } = req.params; let checklist = await EorChecklist.findOne({ - where: { applicationId }, + where: relocationId ? { relocationId } : { applicationId }, include: [{ model: EorChecklistItem, as: 'items', include: ['proofDocument'] }] }); @@ -25,32 +25,62 @@ export const getChecklist = async (req: Request, res: Response) => { export const createChecklist = async (req: AuthRequest, res: Response) => { try { - const { applicationId } = req.body; + const { applicationId, relocationId } = req.body; - const application = await db.Application.findByPk(applicationId); - if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); + if (!applicationId && !relocationId) { + return res.status(400).json({ success: false, message: 'applicationId or relocationId is required' }); + } + + if (applicationId) { + const application = await db.Application.findByPk(applicationId); + if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); + } else if (relocationId) { + const relocation = await db.RelocationRequest.findByPk(relocationId); + if (!relocation) return res.status(404).json({ success: false, message: 'Relocation request not found' }); + } const [checklist, created] = await EorChecklist.findOrCreate({ - where: { applicationId }, - defaults: { status: 'In Progress' } + where: relocationId ? { relocationId } : { applicationId }, + defaults: { + status: 'In Progress', + applicationId: applicationId || null, + relocationId: relocationId || null + } }); if (created) { // Define Default Mandatory Items per SRS/Frontend - const defaultItems = [ - { itemType: 'Sales', description: 'Sales Standards' }, - { itemType: 'Service', description: 'Service & Spares' }, - { itemType: 'IT', description: 'DMS infra' }, - { itemType: 'Training', description: 'Manpower Training' }, - { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' }, - { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' }, - { itemType: 'Finance', description: 'Inventory Funding' }, - { itemType: 'IT', description: 'Virtual code availability' }, - { itemType: 'Finance', description: 'Vendor payments' }, - { itemType: 'Marketing', description: 'Details for website submission' }, - { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' }, - { itemType: 'IT', description: 'Auto ordering' } - ]; + let defaultItems = []; + + if (relocationId) { + // Strictly per SRS Section 12.2.8 for Relocation + defaultItems = [ + { itemType: 'Property', description: 'Property documents for new location' }, + { itemType: 'Property', description: 'Lease / Rental agreement' }, + { itemType: 'Property', description: 'Layout / Floor plan of new location' }, + { itemType: 'Infrastructure', description: 'Photos of new location' }, + { itemType: 'Infrastructure', description: 'Locality map / Building plan approval' }, + { itemType: 'Statutory', description: 'NOC from current landlord' }, + { itemType: 'Statutory', description: 'Municipal approvals (Fire safety / Pollution clearance)' }, + { itemType: 'Utility', description: 'Electricity & Water supply documents' } + ]; + } else { + // Onboarding Default + defaultItems = [ + { itemType: 'Sales', description: 'Sales Standards' }, + { itemType: 'Service', description: 'Service & Spares' }, + { itemType: 'IT', description: 'DMS infra' }, + { itemType: 'Training', description: 'Manpower Training' }, + { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' }, + { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' }, + { itemType: 'Finance', description: 'Inventory Funding' }, + { itemType: 'IT', description: 'Virtual code availability' }, + { itemType: 'Finance', description: 'Vendor payments' }, + { itemType: 'Marketing', description: 'Details for website submission' }, + { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' }, + { itemType: 'IT', description: 'Auto ordering' } + ]; + } const itemsData = defaultItems.map(item => ({ ...item, @@ -113,15 +143,25 @@ export const submitAudit = async (req: AuthRequest, res: Response) => { if (status === 'Completed') { const checklist = await EorChecklist.findByPk(checklistId); if (checklist) { - await db.Application.update({ - overallStatus: 'Approved', - progressPercentage: 100 - }, { where: { id: checklist.applicationId } }); + if (checklist.applicationId) { + await db.Application.update({ + overallStatus: 'Approved', + progressPercentage: 100 + }, { where: { id: checklist.applicationId } }); - // Update Progress Tracking - const { updateApplicationProgress } = await import('../../common/utils/progress.js'); - await updateApplicationProgress(checklist.applicationId, 'EOR Complete', 'completed', 100); - await updateApplicationProgress(checklist.applicationId, 'Inauguration', 'active', 50); + // Update Progress Tracking + const { updateApplicationProgress } = await import('../../common/utils/progress.js'); + await updateApplicationProgress(checklist.applicationId, 'EOR Complete', 'completed', 100); + await updateApplicationProgress(checklist.applicationId, 'Inauguration', 'active', 50); + } else if (checklist.relocationId) { + await db.RelocationRequest.update({ + status: 'Completed', + progressPercentage: 100, + currentStage: 'Completed' + }, { where: { id: checklist.relocationId } }); + + // The workflow service can handle timeline/audit but here we just finalized the status + } } } diff --git a/src/modules/eor/eor.routes.ts b/src/modules/eor/eor.routes.ts index ae2f0a8..c0d1a50 100644 --- a/src/modules/eor/eor.routes.ts +++ b/src/modules/eor/eor.routes.ts @@ -5,7 +5,8 @@ import { authenticate } from '../../common/middleware/auth.js'; router.use(authenticate as any); -router.get('/:applicationId', eorController.getChecklist); +router.get('/application/:applicationId', eorController.getChecklist); +router.get('/relocation/:relocationId', eorController.getChecklist); router.post('/', eorController.createChecklist); router.post('/item/:checklistId', eorController.updateItem); router.post('/audit/:checklistId', eorController.submitAudit); diff --git a/src/modules/loa/loa.controller.ts b/src/modules/loa/loa.controller.ts index 4130c64..f6f34c3 100644 --- a/src/modules/loa/loa.controller.ts +++ b/src/modules/loa/loa.controller.ts @@ -272,6 +272,68 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) => }); } + // AUTOMATION: If Initial Payment is Verified, auto-approve "Finance" role in LOI Stage + if (depositType === 'INITIAL' && status === 'Verified') { + const LoiRequest = db.LoiRequest; + const LoiApproval = db.LoiApproval; + const StageApprovalAction = db.StageApprovalAction; + + const loiReq = await LoiRequest.findOne({ where: { applicationId: application.id } }); + if (loiReq) { + // 1. Update module-specific approval table + const financeApproval = await LoiApproval.findOne({ + where: { requestId: loiReq.id, approverRole: 'Finance', action: 'Pending' } + }); + if (financeApproval) { + await financeApproval.update({ + action: 'Approved', + remarks: 'Auto-approved via Finance payment verification', + approverId: req.user?.id, + approvedAt: new Date() + }); + } + + // 2. Update generic StageApprovalAction table + await StageApprovalAction.upsert({ + applicationId: application.id, + stageCode: 'LOI_APPROVAL', + actorUserId: req.user?.id, + actorRole: 'Finance', + decision: 'Approved', + remarks: 'Auto-approved via Finance payment verification' + }); + + // 3. Check if LOI can now be fully approved (copied logic from loi.controller) + const policy = await db.StageApprovalPolicy.findOne({ where: { stageCode: 'LOI_APPROVAL' } }); + const requiredRoles = policy?.requiredRoles || ['Finance', 'DD Head', 'NBH']; + + const stageActions = await StageApprovalAction.findAll({ + where: { applicationId: application.id, stageCode: 'LOI_APPROVAL' } + }); + const approvedRoles = new Set(stageActions.filter((a: any) => a.decision === 'Approved').map((a: any) => a.actorRole)); + const meetsMinApprovals = approvedRoles.size >= (policy?.minApprovals || 3); + const hasAllRequired = requiredRoles.every((r: string) => approvedRoles.has(r)); + + if (hasAllRequired && meetsMinApprovals && loiReq.status !== 'Approved') { + await loiReq.update({ status: 'Approved', approvedBy: req.user?.id, approvedAt: new Date() }); + + const mockFile = `LOI_${loiReq.id}.pdf`; + await db.LoiDocumentGenerated.findOrCreate({ + where: { requestId: loiReq.id, documentType: 'LOI' }, + defaults: { + fileName: mockFile, + filePath: `/uploads/loi/${mockFile}` + } + }); + + await WorkflowService.transitionApplication(application, APPLICATION_STATUS.SECURITY_DETAILS, req.user?.id, { + reason: 'LOI Request fully approved via automated finance verification', + progressPercentage: 80 + }); + } + } + } + res.json({ success: true, message: 'Security Deposit updated', data: deposit }); } catch (error) { console.error('Update Security Deposit error:', error); diff --git a/src/modules/master/master.routes.ts b/src/modules/master/master.routes.ts index 3c2ef03..840a07a 100644 --- a/src/modules/master/master.routes.ts +++ b/src/modules/master/master.routes.ts @@ -64,6 +64,7 @@ router.get('/area-managers', getAreaManagers); router.get('/asms', getASMs); router.get('/zonal-managers', getZonalManagers); router.post('/zonal-managers', saveZM); +router.get('/dd-leads', getDDLeads); router.post('/dd-leads', saveDDLead); router.get('/system-configs', getSystemConfigs); router.post('/system-configs', saveSystemConfig); diff --git a/src/modules/self-service/constitutional.controller.ts b/src/modules/self-service/constitutional.controller.ts index 156f111..0f8855a 100644 --- a/src/modules/self-service/constitutional.controller.ts +++ b/src/modules/self-service/constitutional.controller.ts @@ -50,7 +50,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => { if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); const where: any = {}; - if (req.user.role === 'Dealer') { + if (req.user.roleCode === 'Dealer') { where.dealerId = req.user.id; } diff --git a/src/modules/self-service/relocation.controller.ts b/src/modules/self-service/relocation.controller.ts index 1fa09f7..5c22db6 100644 --- a/src/modules/self-service/relocation.controller.ts +++ b/src/modules/self-service/relocation.controller.ts @@ -1,10 +1,12 @@ import { Response } from 'express'; import db from '../../database/models/index.js'; -const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone } = db; +import { RelocationWorkflowService } from '../../services/RelocationWorkflowService.js'; +const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone, Document } = db; import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js'; 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'; /** * Helper to assign evaluators for relocation requests based on outlet location hierarchy @@ -48,22 +50,22 @@ const assignRelocationEvaluators = async (requestId: string, outletId: string) = // Stage 1: DD ASM (from district) if (district.asmId) { - evaluators.push({ id: district.asmId, role: 'ASM', stage: 'ASM_REVIEW' }); + evaluators.push({ id: district.asmId, role: 'ASM', stage: RELOCATION_STAGES.ASM_REVIEW }); } // Stage 2: RBM (from region) if (region && region.rbmId) { - evaluators.push({ id: region.rbmId, role: 'RBM', stage: 'RBM_REVIEW' }); + evaluators.push({ id: region.rbmId, role: 'RBM', stage: RELOCATION_STAGES.RBM_REVIEW }); } // Stage 3: DD ZM (from district) if (district.zmId) { - evaluators.push({ id: district.zmId, role: 'DD-ZM', stage: 'DD_ZM_REVIEW' }); + evaluators.push({ id: district.zmId, role: 'DD-ZM', stage: RELOCATION_STAGES.DD_ZM_REVIEW }); } // Stage 4: ZBH (from zone) if (zone && zone.zbhId) { - evaluators.push({ id: zone.zbhId, role: 'ZBH', stage: 'ZBH_REVIEW' }); + evaluators.push({ id: zone.zbhId, role: 'ZBH', stage: RELOCATION_STAGES.ZBH_REVIEW }); } // Stage 5: DD Lead (zone-scoped) @@ -77,30 +79,35 @@ const assignRelocationEvaluators = async (requestId: string, outletId: string) = }] }); if (ddLead) { - evaluators.push({ id: ddLead.id, role: 'DD Lead', stage: 'DD_LEAD_REVIEW' }); + evaluators.push({ id: ddLead.id, role: 'DD Lead', stage: RELOCATION_STAGES.DD_LEAD_REVIEW }); } } - // Stage 6: NBH (national) - const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } }); - if (nbh) { - evaluators.push({ id: nbh.id, role: 'NBH', stage: 'NBH_REVIEW' }); + // Stage 6: DD Head (national) + const ddHead = await User.findOne({ where: { roleCode: 'DD Head', status: 'active' } }); + if (ddHead) { + evaluators.push({ id: ddHead.id, role: 'DD Head', stage: RELOCATION_STAGES.DD_HEAD_APPROVAL }); } - // Stage 7: Legal (national) + // Stage 7: NBH Approval (national) + const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } }); + if (nbh) { + evaluators.push({ id: nbh.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_APPROVAL }); + } + + // Stage 8: Legal Clearance (national) const legal = await User.findOne({ where: { roleCode: 'Legal Admin', status: 'active' } }); if (legal) { - evaluators.push({ id: legal.id, role: 'Legal', stage: 'LEGAL_CLEARANCE' }); + evaluators.push({ id: legal.id, role: 'Legal', stage: RELOCATION_STAGES.LEGAL_CLEARANCE }); + } + + // Stage 9: NBH Clearance with EOR (national) + if (nbh) { + evaluators.push({ id: nbh.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_CLEARANCE_EOR }); } console.log(`[debug] Found ${evaluators.length} evaluators for relocation request`); - // Note: RequestParticipant table has FK to applications, not relocation_requests - // So we store evaluators directly in the relocation request's timeline/metadata - // and return them via the outlet's location hierarchy lookup - - // Store evaluator info in a separate table or return via API - // For now, log and store in request metadata via timeline const evaluatorInfo = evaluators.map(e => ({ userId: e.id, role: e.role, @@ -108,13 +115,9 @@ const assignRelocationEvaluators = async (requestId: string, outletId: string) = })); console.log(`[debug] Evaluators assigned:`, evaluatorInfo); - console.log(`[debug] Successfully assigned ${evaluators.length} evaluators to relocation request`); - - // Return evaluator info in response return evaluatorInfo; } catch (error) { console.error('[debug] Error assigning relocation evaluators:', error); - // Don't throw - assignment is non-critical } }; @@ -152,9 +155,9 @@ export const submitRequest = async (req: AuthRequest, res: Response) => { newDistrictId: newDistrictId || null, newStateId: newStateId || null, reason, - currentStage: RELOCATION_STAGES.DD_ADMIN_REVIEW as any, - status: 'Pending', - progressPercentage: 20, + currentStage: RELOCATION_STAGES.ASM_REVIEW, + status: 'Pending ASM Review', + progressPercentage: 10, documents: [], timeline: [{ stage: 'Submitted', @@ -183,7 +186,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => { if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); const where: any = {}; - if (req.user.role === 'Dealer') { + if (req.user.roleCode === 'Dealer') { where.dealerId = req.user.id; } @@ -193,7 +196,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => { { model: Outlet, as: 'outlet', - attributes: ['code', 'name'], + attributes: ['code', 'name', 'address', 'city', 'state', 'pincode'], include: [ { model: District, @@ -218,7 +221,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => { // Filter requests based on user's role and location assignments const filteredRequests = requests.filter((request: any) => { // Dealers see only their own requests - if (req.user?.role === 'Dealer') { + if (req.user?.roleCode === 'Dealer') { return request.dealerId === req.user.id; } @@ -247,8 +250,20 @@ export const getRequests = async (req: AuthRequest, res: Response) => { return isAssigned; }); + + // Enrich responses with currentLocation and proposedLocation for frontend + const enrichedRequests = filteredRequests.map((request: any) => { + const reqData = request.get({ plain: true }); + const outlet = reqData.outlet; + + return { + ...reqData, + currentLocation: outlet ? `${outlet.address}, ${outlet.city}, ${outlet.state} - ${outlet.pincode}` : 'N/A', + proposedLocation: `${reqData.newAddress}, ${reqData.newCity}, ${reqData.newState}` + }; + }); - res.json({ success: true, requests: filteredRequests }); + res.json({ success: true, requests: enrichedRequests }); } catch (error) { console.error('Get relocation requests error:', error); res.status(500).json({ success: false, message: 'Error fetching requests' }); @@ -305,11 +320,11 @@ export const getRequestById = async (req: AuthRequest, res: Response) => { const region = district.region; const zone = district.zone; - const evaluatorRoles = [ - { id: district.asmId, role: 'ASM', stage: 'ASM_REVIEW' }, - { id: region?.rbmId, role: 'RBM', stage: 'RBM_REVIEW' }, - { id: district.zmId, role: 'DD-ZM', stage: 'DD_ZM_REVIEW' }, - { id: zone?.zbhId, role: 'ZBH', stage: 'ZBH_REVIEW' } + const evaluatorRoles: any[] = [ + { id: district.asmId, roleCode: 'ASM', stage: RELOCATION_STAGES.ASM_REVIEW }, + { id: region?.rbmId, roleCode: 'RBM', stage: RELOCATION_STAGES.RBM_REVIEW }, + { id: district.zmId, roleCode: 'DD-ZM', stage: RELOCATION_STAGES.DD_ZM_REVIEW }, + { id: zone?.zbhId, roleCode: 'ZBH', stage: RELOCATION_STAGES.ZBH_REVIEW } ]; // Get DD Lead (zone-scoped) @@ -320,18 +335,29 @@ export const getRequestById = async (req: AuthRequest, res: Response) => { model: db.UserRole, as: 'userRoles', where: { zoneId: zone.id, isActive: true } - }], - attributes: ['id', 'fullName', 'email', 'roleCode'] + }] }); - if (ddLead) evaluatorRoles.push({ id: ddLead.id, role: 'DD Lead', stage: 'DD_LEAD_REVIEW' }); + if (ddLead) evaluatorRoles.push({ id: ddLead.id, roleCode: 'DD Lead', stage: RELOCATION_STAGES.DD_LEAD_REVIEW }); } - // Get NBH and Legal (national) - const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' }, attributes: ['id', 'fullName', 'email', 'roleCode'] }); - if (nbh) evaluatorRoles.push({ id: nbh.id, role: 'NBH', stage: 'NBH_REVIEW' }); + // Get DD Head (national) + const ddHead = await User.findOne({ where: { roleCode: 'DD Head', status: 'active' } }); + if (ddHead) evaluatorRoles.push({ id: ddHead.id, roleCode: 'DD Head', stage: RELOCATION_STAGES.DD_HEAD_APPROVAL }); - const legal = await User.findOne({ where: { roleCode: 'Legal Admin', status: 'active' }, attributes: ['id', 'fullName', 'email', 'roleCode'] }); - if (legal) evaluatorRoles.push({ id: legal.id, role: 'Legal', stage: 'LEGAL_CLEARANCE' }); + // Get NBH (national) - Approval Stage + const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } }); + if (nbh) { + evaluatorRoles.push({ id: nbh.id, roleCode: 'NBH', stage: RELOCATION_STAGES.NBH_APPROVAL }); + } + + // Get Legal (national) + const legal = await User.findOne({ where: { roleCode: 'Legal Admin', status: 'active' } }); + if (legal) evaluatorRoles.push({ id: legal.id, roleCode: 'Legal Admin', stage: RELOCATION_STAGES.LEGAL_CLEARANCE }); + + // Get NBH (national) - Final Clearance Stage + if (nbh) { + evaluatorRoles.push({ id: nbh.id, roleCode: 'NBH', stage: RELOCATION_STAGES.NBH_CLEARANCE_EOR }); + } // Fetch user details for each evaluator for (const evaluator of evaluatorRoles) { @@ -342,7 +368,7 @@ export const getRequestById = async (req: AuthRequest, res: Response) => { id: `eval-${evaluator.stage}`, userId: evaluator.id, participantType: 'reviewer', - metadata: { stage: evaluator.stage, role: evaluator.role, autoAssigned: true }, + metadata: { stage: evaluator.stage, role: evaluator.roleCode, autoAssigned: true }, user }); } @@ -350,7 +376,10 @@ export const getRequestById = async (req: AuthRequest, res: Response) => { } } + // Enrich response with currentLocation and proposedLocation const response = request.toJSON(); + (response as any).currentLocation = (response as any).outlet ? `${(response as any).outlet.address}, ${(response as any).outlet.city}, ${(response as any).outlet.state} - ${(response as any).outlet.pincode}` : 'N/A'; + (response as any).proposedLocation = `${(response as any).newAddress}, ${(response as any).newCity}, ${(response as any).newState}`; (response as any).participants = participants; res.json({ success: true, request: response }); @@ -367,51 +396,99 @@ export const takeAction = async (req: AuthRequest, res: Response) => { const { id } = req.params; const { action, comments } = req.body; - // Only search by requestId since frontend sends requestId, not UUID + // Normalize action to uppercase for service consistency (APPROVE/REJECT) + const normalizedAction = action?.toUpperCase() || ''; + + // 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 request = await RelocationRequest.findOne({ - where: { - requestId: id - } + where: isUUID ? { id: idStr } : { requestId: idStr } }); if (!request) { return res.status(404).json({ success: false, message: 'Request not found' }); } + // 1. Authorization Check via Workflow Service + const canAction = await RelocationWorkflowService.canUserAction(request, req.user); + if (!canAction) { + return res.status(403).json({ + success: false, + message: `Forbidden: Your role (${req.user?.roleCode}) is not authorized to take actions for the current stage: ${request.currentStage}` + }); + } + // Update status and current_stage based on action let newStatus = request.status; let newCurrentStage = request.currentStage; const stageFlow: Record = { - [RELOCATION_STAGES.DD_ADMIN_REVIEW]: RELOCATION_STAGES.RBM_REVIEW, - [RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.NBH_APPROVAL, + [RELOCATION_STAGES.ASM_REVIEW]: RELOCATION_STAGES.RBM_REVIEW, + [RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.DD_ZM_REVIEW, + [RELOCATION_STAGES.DD_ZM_REVIEW]: RELOCATION_STAGES.ZBH_REVIEW, + [RELOCATION_STAGES.ZBH_REVIEW]: RELOCATION_STAGES.DD_LEAD_REVIEW, + [RELOCATION_STAGES.DD_LEAD_REVIEW]: RELOCATION_STAGES.DD_HEAD_APPROVAL, + [RELOCATION_STAGES.DD_HEAD_APPROVAL]: RELOCATION_STAGES.NBH_APPROVAL, [RELOCATION_STAGES.NBH_APPROVAL]: RELOCATION_STAGES.LEGAL_CLEARANCE, - [RELOCATION_STAGES.LEGAL_CLEARANCE]: RELOCATION_STAGES.COMPLETED + [RELOCATION_STAGES.LEGAL_CLEARANCE]: RELOCATION_STAGES.NBH_CLEARANCE_EOR, + [RELOCATION_STAGES.NBH_CLEARANCE_EOR]: RELOCATION_STAGES.COMPLETED }; - if (action === 'Approved') { + if (normalizedAction === 'APPROVE') { newCurrentStage = stageFlow[request.currentStage] || request.currentStage; - newStatus = newCurrentStage === RELOCATION_STAGES.COMPLETED ? 'Completed' : `Pending ${newCurrentStage.replace('_', ' ')}`; - } else if (action === 'Rejected') { + newStatus = newCurrentStage === RELOCATION_STAGES.COMPLETED ? 'Completed' : `Pending ${newCurrentStage}`; + } else if (normalizedAction === 'REJECT') { newStatus = 'Rejected'; newCurrentStage = RELOCATION_STAGES.REJECTED; } - // Create a worknote entry - await Worknote.create({ - requestId: request.id, - requestType: 'relocation' as any, - userId: req.user.id, - content: comments, - isInternal: true + // 2. Perform transition via Workflow Service (handles request update, timeline, audit logs) + const progressSteps = 9; + const currentStepIndex = Object.keys(stageFlow).indexOf(request.currentStage); + const newProgress = normalizedAction === 'APPROVE' + ? Math.min(Math.round(((currentStepIndex + 2) / progressSteps) * 100), 100) + : request.progressPercentage; + + await RelocationWorkflowService.transitionRelocation(request, newStatus, req.user?.id || null, { + reason: comments || 'No remarks provided', + stage: newCurrentStage, + action: normalizedAction, + progressPercentage: newProgress }); - // Update the request status and current stage - await request.update({ - status: newStatus, - currentStage: newCurrentStage, - updatedAt: new Date() - }); + // 2.5 Auto-initiate EOR Checklist if moving to NBH_CLEARANCE_EOR + if (newCurrentStage === RELOCATION_STAGES.NBH_CLEARANCE_EOR && normalizedAction === 'APPROVE') { + try { + // Internal call to EOR controller logic (or we could use a service) + // For now, simpler to just trigger the DB creation here or ensure controller handles it + const { createChecklist } = await import('../eor/eor.controller.js'); + // We mock the req/res for internal call or just use the DB directly + await db.EorChecklist.findOrCreate({ + where: { relocationId: request.id }, + defaults: { + status: 'In Progress', + relocationId: request.id, + applicationId: null + } + }); + console.log(`[RelocationController] EOR Checklist initiated for ${request.requestId}`); + } catch (e) { + console.error('Failed to auto-initiate EOR checklist:', e); + } + } + + // 3. Create a worknote entry for the comment + if (comments) { + await Worknote.create({ + requestId: request.id, + requestType: 'relocation' as any, + userId: req.user.id, + content: comments, + isInternal: true + }); + } res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` }); } catch (error) { @@ -423,31 +500,165 @@ export const takeAction = async (req: AuthRequest, res: Response) => { export const uploadDocuments = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { documents } = req.body; + const { documentType, stage } = req.body; + const file = req.file; + + if (!file) { + return res.status(400).json({ success: false, message: 'No file uploaded' }); + } + + if (!documentType) { + return res.status(400).json({ success: false, message: 'Document type is required' }); + } // Only search by requestId since frontend sends requestId, not UUID const request = await RelocationRequest.findOne({ where: { - requestId: id + [Op.or]: [ + { id }, + { requestId: id } + ] } }); if (!request) { - return res.status(404).json({ success: false, message: 'Request not found' }); + return res.status(404).json({ success: false, message: 'Relocation request not found' }); } + // Create Document Record + const newDoc = await Document.create({ + requestId: request.id, + requestType: 'relocation', + documentType, + stage: stage || request.currentStage, + fileName: file.originalname, + filePath: file.path, + fileSize: file.size, + mimeType: file.mimetype, + uploadedBy: req.user?.id, + status: 'active' + }); + + // Update the documents JSON array in RelocationRequest for quick access/legacy compatibility + const currentDocuments = request.documents || []; + const updatedDocuments = [...currentDocuments, { + id: newDoc.id, + name: file.originalname, + type: documentType, + url: newDoc.filePath, // Critical for frontend preview/download + mimeType: newDoc.mimeType, // Useful for previewer + uploadedOn: new Date(), + uploadedBy: req.user?.fullName || 'System', + status: 'Pending Verification', + category: 'Relocation' + }]; + await request.update({ - documents: documents, + documents: updatedDocuments, updatedAt: new Date() }); - res.json({ success: true, message: 'Documents uploaded successfully' }); + res.status(201).json({ + success: true, + message: 'Document uploaded successfully', + data: newDoc + }); } catch (error) { console.error('Upload documents error:', error); res.status(500).json({ success: false, message: 'Error uploading documents' }); } }; +export const verifyDocument = async (req: AuthRequest, res: Response) => { + try { + const { id, documentId } = req.params; + + if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); + + // Search by UUID or requestId for the request + const request = await RelocationRequest.findOne({ + where: { + [Op.or]: [ + { id }, + { requestId: id } + ] + } + }); + + if (!request) { + return res.status(404).json({ success: false, message: 'Relocation request not found' }); + } + + // Authorization: Non-dealers only, and ideally matching the current stage + const isInternal = req.user.roleCode !== 'Dealer'; + if (!isInternal) { + return res.status(403).json({ success: false, message: 'Forbidden: Dealers cannot verify documents' }); + } + + // Find and update the Document record + const docRecord = await Document.findByPk(documentId); + if (docRecord) { + await docRecord.update({ status: 'Verified' }); + } + + // Update the document entry in the request's JSON JSON array + const currentDocuments = request.documents || []; + const documentIndex = currentDocuments.findIndex((d: any) => d.id === documentId); + + if (documentIndex !== -1) { + currentDocuments[documentIndex].status = 'Verified'; + currentDocuments[documentIndex].verifiedBy = req.user.fullName; + currentDocuments[documentIndex].verifiedOn = new Date(); + + // Add simple timeline log for document verification in same update + const updatedTimeline = [...(request.timeline || []), { + stage: request.currentStage, + timestamp: new Date(), + user: req.user.fullName, + action: 'Document Verified', + remarks: `Verified document: ${currentDocuments[documentIndex].name}` + }]; + + // Calculate progress percentage + const totalRequired = 12; // Standard relocation requirement + const verifiedCount = currentDocuments.filter((d: any) => d.status === 'Verified').length; + const progressPercentage = Math.min(Math.round((verifiedCount / totalRequired) * 100), 100); + + // Update request status to 'In Progress' if it was 'Pending' + let newStatus = request.status; + if (request.status === 'Pending') { + newStatus = 'In Progress'; + } + + await request.update({ + documents: currentDocuments, + timeline: updatedTimeline, + progressPercentage, + status: newStatus, + updatedAt: new Date() + }); + + // Force Sequelize to detect JSON changes + request.changed('documents', true); + request.changed('timeline', true); + await request.save(); + + return res.json({ + success: true, + message: 'Document verified successfully', + document: currentDocuments[documentIndex], + progressPercentage, + status: newStatus + }); + } + + res.status(404).json({ success: false, message: 'Document not found in request tracker' }); + } catch (error) { + console.error('Verify document error:', error); + res.status(500).json({ success: false, message: 'Error verifying document' }); + } +}; + // Helper function to calculate distance between two coordinates function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371; // Radius of Earth in km diff --git a/src/modules/self-service/relocation.routes.ts b/src/modules/self-service/relocation.routes.ts index 9c6abbc..133b7d3 100644 --- a/src/modules/self-service/relocation.routes.ts +++ b/src/modules/self-service/relocation.routes.ts @@ -3,12 +3,14 @@ const router = express.Router(); import * as relocationController from './relocation.controller.js'; import { authenticate } from '../../common/middleware/auth.js'; +import { uploadSingle } from '../../common/middleware/upload.js'; // Relocation routes router.post('/', authenticate as any, relocationController.submitRequest); router.get('/', authenticate as any, relocationController.getRequests); router.get('/:id', authenticate as any, relocationController.getRequestById); -router.put('/:id/action', authenticate as any, relocationController.takeAction); -router.post('/:id/documents', authenticate as any, relocationController.uploadDocuments); +router.post('/:id/action', authenticate as any, relocationController.takeAction); +router.post('/:id/documents', authenticate as any, uploadSingle, relocationController.uploadDocuments); +router.post('/:id/documents/:documentId/verify', authenticate as any, relocationController.verifyDocument); export default router; \ No newline at end of file diff --git a/src/modules/self-service/resignation.controller.ts b/src/modules/self-service/resignation.controller.ts index 851c4be..4d4158a 100644 --- a/src/modules/self-service/resignation.controller.ts +++ b/src/modules/self-service/resignation.controller.ts @@ -139,7 +139,7 @@ export const getResignations = async (req: AuthRequest, res: Response, next: Nex // Build where clause based on user role let where: any = {}; - if (req.user.role === ROLES.DEALER) { + if (req.user.roleCode === ROLES.DEALER) { where.dealerId = req.user.id; } @@ -219,7 +219,7 @@ export const getResignationById = async (req: AuthRequest, res: Response, next: } // Check access permissions - if (req.user.role === ROLES.DEALER && resignation.dealerId !== req.user.id) { + if (req.user.roleCode === ROLES.DEALER && resignation.dealerId !== req.user.id) { return res.status(403).json({ success: false, message: 'Access denied' diff --git a/src/modules/self-service/self-service.routes.ts b/src/modules/self-service/self-service.routes.ts index cc02c1e..5ba362a 100644 --- a/src/modules/self-service/self-service.routes.ts +++ b/src/modules/self-service/self-service.routes.ts @@ -13,14 +13,14 @@ router.use('/resignations', resignationRoutes); router.post('/constitutional', authenticate as any, constitutionalController.submitRequest); router.get('/constitutional', authenticate as any, constitutionalController.getRequests); router.get('/constitutional/:id', authenticate as any, constitutionalController.getRequestById); -router.put('/constitutional/:id/action', authenticate as any, constitutionalController.takeAction); +router.post('/constitutional/:id/action', authenticate as any, constitutionalController.takeAction); router.post('/constitutional/:id/documents', authenticate as any, constitutionalController.uploadDocuments); // Relocation submodule router.post('/relocation', authenticate as any, relocationController.submitRequest); router.get('/relocation', authenticate as any, relocationController.getRequests); router.get('/relocation/:id', authenticate as any, relocationController.getRequestById); -router.put('/relocation/:id/action', authenticate as any, relocationController.takeAction); +router.post('/relocation/:id/action', authenticate as any, relocationController.takeAction); router.post('/relocation/:id/documents', authenticate as any, relocationController.uploadDocuments); export default router; diff --git a/src/services/RelocationWorkflowService.ts b/src/services/RelocationWorkflowService.ts new file mode 100644 index 0000000..deb1545 --- /dev/null +++ b/src/services/RelocationWorkflowService.ts @@ -0,0 +1,91 @@ +import db from '../database/models/index.js'; +const { RelocationRequest, AuditLog, User } = db; +import { AUDIT_ACTIONS, RELOCATION_STAGES, ROLES } from '../common/config/constants.js'; + +export class RelocationWorkflowService { + /** + * Standardized method to transition a relocation request status + */ + static async transitionRelocation(request: any, targetStatus: string, userId: string | null = null, metadata: any = {}) { + const previousStatus = request.status; + const { reason, stage, progressPercentage, action } = metadata; + + const updateData: any = { + status: targetStatus, + updatedAt: new Date() + }; + + // Update stage if provided and valid + if (stage && Object.values(RELOCATION_STAGES).includes(stage)) { + updateData.currentStage = stage; + } + + // Update progress percentage if explicitly provided + if (progressPercentage !== undefined) { + updateData.progressPercentage = progressPercentage; + } + + // 1. Update Request Record + await request.update(updateData); + + // 2. Update Timeline (JSON array) + const user = userId ? await User.findByPk(userId) : null; + const timelineEntry = { + stage: stage || request.currentStage, + timestamp: new Date(), + user: user ? user.fullName : 'System', + action: action || `Transitioned to ${targetStatus}`, + remarks: reason || '' + }; + + const updatedTimeline = [...(request.timeline || []), timelineEntry]; + await request.update({ timeline: updatedTimeline }); + + // 3. Create Audit Log + await AuditLog.create({ + userId: userId, + action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED, + entityType: 'relocation', + entityId: request.id, + newData: { status: targetStatus, stage: stage || request.currentStage, reason } + }); + + console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`); + + return request; + } + + /** + * Checks if a user is authorized to perform an action in the current stage + */ + static async canUserAction(request: any, user: any) { + if (!user) return false; + + // Super Admin bypass + if (user.roleCode === ROLES.SUPER_ADMIN) return true; + + const stageMapping: Record = { + [RELOCATION_STAGES.ASM_REVIEW]: ROLES.ASM, + [RELOCATION_STAGES.RBM_REVIEW]: ROLES.RBM, + [RELOCATION_STAGES.DD_ZM_REVIEW]: ROLES.DD_ZM, + [RELOCATION_STAGES.ZBH_REVIEW]: ROLES.ZBH, + [RELOCATION_STAGES.DD_LEAD_REVIEW]: ROLES.DD_LEAD, + [RELOCATION_STAGES.DD_HEAD_APPROVAL]: ROLES.DD_HEAD, + [RELOCATION_STAGES.NBH_APPROVAL]: ROLES.NBH, + [RELOCATION_STAGES.LEGAL_CLEARANCE]: ROLES.LEGAL_ADMIN, + [RELOCATION_STAGES.NBH_CLEARANCE_EOR]: ROLES.NBH + }; + + const requiredRole = stageMapping[request.currentStage]; + if (!requiredRole) return false; + + // Role-based check + if (user.roleCode !== requiredRole) return false; + + // Optional: Hierarchy check + // We could verify if the user is the SPECIFIC person assigned in participants + // but for now, any user with the correct role can act (consistent with simple RBAC) + + return true; + } +} diff --git a/verify_relocation_auth.ts b/verify_relocation_auth.ts new file mode 100644 index 0000000..f88da93 --- /dev/null +++ b/verify_relocation_auth.ts @@ -0,0 +1,55 @@ +import db from './src/database/models/index.js'; +import { RELOCATION_STAGES, ROLES } from './src/common/config/constants.js'; +import { RelocationWorkflowService } from './src/services/RelocationWorkflowService.js'; + +const { RelocationRequest, User } = db; + +async function run() { + try { + console.log('--- Relocation Authorization Verification ---'); + + // 1. Find a test request + const request = await RelocationRequest.findOne({ + order: [['createdAt', 'DESC']] + }); + + if (!request) { + console.log('No relocation request found to test.'); + process.exit(0); + } + + console.log(`Testing Request: ${request.requestId}, Current Stage: ${request.currentStage}`); + + // 2. Find users with different roles + const asmUser = await User.findOne({ where: { roleCode: ROLES.ASM, status: 'active' } }); + const rbmUser = await User.findOne({ where: { roleCode: ROLES.RBM, status: 'active' } }); + const dealerUser = await User.findOne({ where: { roleCode: ROLES.DEALER, status: 'active' } }); + const superAdmin = await User.findOne({ where: { roleCode: ROLES.SUPER_ADMIN, status: 'active' } }); + + // 3. Test ASM approval on ASM_REVIEW stage + if (request.currentStage === RELOCATION_STAGES.ASM_REVIEW) { + console.log('Testing ASM_REVIEW stage:'); + + const canASM = await RelocationWorkflowService.canUserAction(request, asmUser); + console.log(`- ASM can action: ${canASM} (Expected: true)`); + + const canRBM = await RelocationWorkflowService.canUserAction(request, rbmUser); + console.log(`- RBM can action: ${canRBM} (Expected: false)`); + + const canDealer = await RelocationWorkflowService.canUserAction(request, dealerUser); + console.log(`- Dealer can action: ${canDealer} (Expected: false)`); + + const canSuperAdmin = await RelocationWorkflowService.canUserAction(request, superAdmin); + console.log(`- Super Admin can action: ${canSuperAdmin} (Expected: true)`); + } else { + console.log(`Request is in ${request.currentStage}. Please create a new request to test ASM_REVIEW.`); + } + + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +run(); diff --git a/verify_relocation_workflow.ts b/verify_relocation_workflow.ts new file mode 100644 index 0000000..0bed89b --- /dev/null +++ b/verify_relocation_workflow.ts @@ -0,0 +1,102 @@ +import db from './src/database/models/index.js'; +import { RELOCATION_STAGES } from './src/common/config/constants.js'; + +const { RelocationRequest, Outlet, User, District, Region, Zone } = db; + +async function run() { + try { + console.log('--- Relocation Workflow Verification ---'); + + // 1. Find a test outlet + const outlet = await Outlet.findOne({ + include: [ + { + model: District, + as: 'district', + include: [ + { model: Region, as: 'region' }, + { model: Zone, as: 'zone' } + ] + } + ] + }); + + if (!outlet || !outlet.district) { + console.log('No outlet with district found to test.'); + process.exit(1); + } + + console.log(`Testing with Outlet: ${outlet.code}, District: ${outlet.district.name}`); + + // Create a mock user if needed (the dealer) + const dealer = await User.findOne({ where: { roleCode: 'Dealer' } }); + if (!dealer) { + console.log('No dealer found for testing.'); + process.exit(1); + } + + const { submitRequest } = await import('./src/modules/self-service/relocation.controller.js'); + + // We'll mock the Request/Response if we wanted to call the controller directly, + // but here we just want to verify if the evaluator logic would work. + + // Let's just manually run the assignRelocationEvaluators logic (internal) + // Actually, I'll just check if the Controller's getRequestById would return participants correctly. + + // Let's create a relocation request directly to test + const request = await RelocationRequest.create({ + requestId: 'TEST-REL-' + Date.now(), + outletId: outlet.id, + dealerId: dealer.id, + relocationType: 'Within City', + newAddress: 'New Test Address', + newCity: 'Test City', + newState: 'Test State', + reason: 'Testing workflow assignment', + currentStage: RELOCATION_STAGES.ASM_REVIEW, + status: 'Pending ASM Review', + progressPercentage: 10, + documents: [], + timeline: [] + }); + + console.log(`Created Test Request: ${request.requestId}`); + + // Now call the logic that calculates participants (similar to getRequestById) + // We'll just look at the DB for now to see if DD Head and NBH (dual) would be assigned. + + // Check DD Head + const ddHead = await User.findOne({ where: { roleCode: 'DD Head', status: 'active' } }); + console.log(`DD Head found in DB: ${ddHead ? ddHead.fullName : 'NO'}`); + + // Check NBH + const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } }); + console.log(`NBH found in DB: ${nbh ? nbh.fullName : 'NO'}`); + + // Verify Evaluator Assignment Logic (Re-running a piece of it) + const evaluators = []; + evaluators.push({ id: outlet.district.asmId, role: 'ASM', stage: RELOCATION_STAGES.ASM_REVIEW }); + evaluators.push({ id: ddHead?.id, role: 'DD Head', stage: RELOCATION_STAGES.DD_HEAD_APPROVAL }); + evaluators.push({ id: nbh?.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_APPROVAL }); + evaluators.push({ id: nbh?.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_CLEARANCE }); + + console.log('Expected Evaluators for this request:'); + evaluators.forEach(e => console.log(`- Role: ${e.role}, Stage: ${e.stage}, ID: ${e.id}`)); + + // Check for success: All must have IDs + const missing = evaluators.filter(e => !e.id); + if (missing.length > 0) { + console.warn('WARNING: Missing some evaluators in the DB. Ensure they are seeded!'); + missing.forEach(m => console.log(` Missing: ${m.role}`)); + } else { + console.log('SUCCESS: All hierarchy and national evaluators identified.'); + } + + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } +} + +run();