import { Response, NextFunction } from 'express'; import db from '../../database/models/index.js'; import logger from '../../common/utils/logger.js'; import { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js'; import { Op, Transaction } from 'sequelize'; import { AuthRequest } from '../../types/express.types.js'; import ExternalMocksService from '../../common/utils/externalMocks.service.js'; import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js'; // Generate unique resignation ID const generateResignationId = async (): Promise => { const count = await db.Resignation.count(); return `RES-${String(count + 1).padStart(3, '0')}`; }; // Create resignation request (Dealer only) export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { const transaction: Transaction = await db.sequelize.transaction(); try { if (!req.user) throw new Error('Unauthorized'); const { outletId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo } = req.body; const dealerId = req.user.id; const outlet = await db.Outlet.findOne({ where: { id: outletId, dealerId } }); if (!outlet) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Outlet not found or does not belong to you' }); } const existingResignation = await db.Resignation.findOne({ where: { outletId, status: { [Op.notIn]: ['Completed', 'Rejected'] } } }); if (existingResignation) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'This outlet already has an active resignation request' }); } const resignationId = await generateResignationId(); const resignation = await db.Resignation.create({ resignationId, outletId, dealerId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo, currentStage: RESIGNATION_STAGES.ASM, status: 'ASM Review', progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM), submittedOn: new Date(), documents: [], timeline: [{ stage: 'Submitted', timestamp: new Date(), user: req.user.fullName, action: 'Resignation request submitted' }] }, { transaction }); await outlet.update({ status: 'Pending Resignation' }, { transaction }); await db.AuditLog.create({ userId: req.user.id, action: AUDIT_ACTIONS.CREATED, entityType: 'resignation', entityId: resignation.id }, { transaction }); await transaction.commit(); logger.info(`Resignation request created: ${resignationId} by dealer: ${req.user.email}`); res.status(201).json({ success: true, message: 'Resignation request submitted successfully', resignationId, resignation }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error creating resignation:', error); next(error); } }; // Get all resignation requests export const getResignations = async (req: AuthRequest, res: Response, next: NextFunction) => { try { if (!req.user) throw new Error('Unauthorized'); const where: any = {}; if (req.user.roleCode === ROLES.DEALER) { where.dealerId = req.user.id; } const resignations = await db.Resignation.findAll({ where, include: [{ model: db.Outlet, as: 'outlet' }], order: [['createdAt', 'DESC']] }); res.json({ success: true, resignations }); } catch (error) { logger.error('Error fetching resignations:', error); next(error); } }; // Get resignation by ID 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 resignation = await db.Resignation.findOne({ where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, include: [ { model: db.Outlet, as: 'outlet' }, { model: db.User, as: 'dealer', attributes: ['id', 'fullName', 'email'] }, { model: db.ResignationDocument, as: 'uploadedDocuments' } ] }); if (!resignation) { return res.status(404).json({ success: false, message: 'Resignation not found' }); } res.json({ success: true, resignation }); } catch (error) { logger.error('Error fetching resignation:', error); next(error); } }; // Approve resignation (move to next stage) export const approveResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { const transaction: Transaction = await db.sequelize.transaction(); 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 resignation = await db.Resignation.findOne({ where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } const stageFlow: Record = { [RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM, [RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH, [RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.DD_LEAD, [RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.NBH, [RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN, [RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL, [RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.FNF_INITIATED, [RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED }; const nextStage = stageFlow[resignation.currentStage]; if (!nextStage) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Cannot approve from current stage' }); } // Transition via Workflow Service await ResignationWorkflowService.transitionResignation(resignation, nextStage, req.user.id, { remarks, status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review` }); // Special logic for F&F and Completion if (nextStage === RESIGNATION_STAGES.COMPLETED) { await (resignation as any).outlet.update({ status: 'Closed' }, { transaction }); ExternalMocksService.mockSyncDealerStatusToSap((resignation as any).outlet.code, 'Inactive') .catch(err => logger.error('Error syncing resignation completion to SAP:', err)); } if (nextStage === RESIGNATION_STAGES.FNF_INITIATED) { const today = new Date(); const lwd = resignation.lastOperationalDateServices || resignation.lastOperationalDateSales; if (lwd && today < new Date(lwd)) { await transaction.rollback(); return res.status(400).json({ success: false, message: `F&F can only be initiated on or after the Last Working Day (${lwd}).` }); } const sapDues = await ExternalMocksService.mockGetFinancialDuesFromSap((resignation as any).outlet.code); const fnf = await db.FnF.create({ resignationId: resignation.id, outletId: resignation.outletId, dealerId: resignation.dealerId, status: 'Initiated', totalReceivables: sapDues.data.outstandingInvoices, totalPayables: sapDues.data.securityDeposit, netAmount: sapDues.data.securityDeposit - sapDues.data.outstandingInvoices }, { transaction }); await db.FnFLineItem.bulkCreate([ { fnfId: fnf.id, itemType: 'Receivable', description: 'Outstanding Invoices from SAP', department: 'Finance', amount: sapDues.data.outstandingInvoices, addedBy: req.user.id }, { fnfId: fnf.id, itemType: 'Payable', description: 'Security Deposit from SAP', department: 'Finance', amount: sapDues.data.securityDeposit, addedBy: req.user.id } ], { transaction }); const { FNF_DEPARTMENTS } = await import('../../common/config/constants.js'); await db.FffClearance.bulkCreate( FNF_DEPARTMENTS.map(dept => ({ fnfId: fnf.id, department: dept, status: 'Pending' })), { transaction } ); } await transaction.commit(); res.json({ success: true, message: 'Resignation approved successfully', nextStage, resignation }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error approving resignation:', error); next(error); } }; // Reject resignation export const rejectResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { const transaction: Transaction = await db.sequelize.transaction(); 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 resignation = await db.Resignation.findOne({ where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } await ResignationWorkflowService.transitionResignation(resignation, RESIGNATION_STAGES.REJECTED, req.user.id, { remarks: reason, action: 'Rejected', status: 'Rejected' }); await (resignation as any).outlet.update({ status: 'Active' }, { transaction }); await transaction.commit(); res.json({ success: true, message: 'Resignation rejected', resignation }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error rejecting resignation:', error); next(error); } }; // Withdraw resignation export const withdrawResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { const transaction: Transaction = await db.sequelize.transaction(); 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 resignation = await db.Resignation.findOne({ where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } const restrictedStages = [RESIGNATION_STAGES.NBH, RESIGNATION_STAGES.DD_ADMIN, RESIGNATION_STAGES.LEGAL, RESIGNATION_STAGES.FNF_INITIATED, RESIGNATION_STAGES.COMPLETED]; if (restrictedStages.includes(resignation.currentStage)) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Withdrawal not allowed after NBH evaluation stage.' }); } await ResignationWorkflowService.transitionResignation(resignation, RESIGNATION_STAGES.REJECTED, req.user.id, { remarks: reason, action: 'Withdrawn', status: 'Withdrawn' }); await (resignation as any).outlet.update({ status: 'Active' }, { transaction }); await transaction.commit(); res.json({ success: true, message: 'Resignation withdrawn successfully' }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error withdrawing resignation:', error); next(error); } }; // Send back resignation export const sendBackResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { const transaction: Transaction = await db.sequelize.transaction(); 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 resignation = await db.Resignation.findOne({ where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } const stageFlowBack: Record = { [RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ASM, [RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.RBM, [RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.ZBH, [RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_LEAD, [RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.NBH, [RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.DD_ADMIN }; const prevStage = targetStage || stageFlowBack[resignation.currentStage]; if (!prevStage) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Cannot send back from current stage' }); } await ResignationWorkflowService.transitionResignation(resignation, prevStage, req.user.id, { remarks, action: 'Sent Back', status: `${prevStage} Review (Sent Back)` }); await transaction.commit(); res.json({ success: true, message: `Resignation sent back to ${prevStage}` }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error sending back resignation:', error); next(error); } }; // Update departmental clearance export const updateClearance = async (req: AuthRequest, res: Response, next: NextFunction) => { const transaction: Transaction = await db.sequelize.transaction(); try { if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; const idStr = String(id); const { department, cleared, 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 resignation = await db.Resignation.findOne({ where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr } }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } const clearances = { ...resignation.departmentalClearances, [department]: cleared }; await resignation.update({ departmentalClearances: clearances, timeline: [...resignation.timeline, { stage: resignation.currentStage, timestamp: new Date(), user: req.user.fullName, action: cleared ? `Cleared ${department}` : `Revoked ${department} clearance`, remarks }] }, { transaction }); await transaction.commit(); res.json({ success: true, message: `Clearance updated for ${department}`, resignation }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error updating clearance:', error); next(error); } };