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'; // Generate unique resignation ID const generateResignationId = async (): Promise => { const count = await db.Resignation.count(); return `RES-${String(count + 1).padStart(3, '0')}`; }; // Calculate progress percentage based on stage const calculateProgress = (stage: string): number => { const stageProgress: Record = { [RESIGNATION_STAGES.ASM]: 15, [RESIGNATION_STAGES.RBM]: 30, [RESIGNATION_STAGES.ZBH]: 40, [RESIGNATION_STAGES.DD_LEAD]: 50, [RESIGNATION_STAGES.NBH]: 60, [RESIGNATION_STAGES.DD_ADMIN]: 65, [RESIGNATION_STAGES.LEGAL]: 70, [RESIGNATION_STAGES.SPARES_CLEARANCE]: 75, [RESIGNATION_STAGES.SERVICE_CLEARANCE]: 80, [RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: 85, [RESIGNATION_STAGES.FINANCE]: 90, [RESIGNATION_STAGES.FNF_INITIATED]: 95, [RESIGNATION_STAGES.COMPLETED]: 100, [RESIGNATION_STAGES.REJECTED]: 0 }; return stageProgress[stage] || 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; // Verify outlet belongs to dealer 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' }); } // Check if outlet already has active resignation 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' }); } // Generate resignation ID const resignationId = await generateResignationId(); // Create resignation const resignation = await db.Resignation.create({ resignationId, outletId, dealerId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo, currentStage: RESIGNATION_STAGES.ASM, status: 'ASM Review', progressPercentage: 15, submittedOn: new Date(), documents: [], timeline: [{ stage: 'Submitted', timestamp: new Date(), user: req.user.fullName, action: 'Resignation request submitted' }] }, { transaction }); // Update outlet status await outlet.update({ status: 'Pending Resignation' }, { transaction }); // Create audit log 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.resignationId, resignation: resignation.toJSON() }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error creating resignation:', error); next(error); } }; // Get resignations list (role-based filtering) export const getResignations = async (req: AuthRequest, res: Response, next: NextFunction) => { try { if (!req.user) throw new Error('Unauthorized'); const { status, page = '1', limit = '10' } = req.query as { status?: string, page?: string, limit?: string }; const offset = (parseInt(page) - 1) * parseInt(limit); // Build where clause based on user role let where: any = {}; if (req.user.role === ROLES.DEALER) { where.dealerId = req.user.id; } if (status) { where.status = status; } // Get resignations const { count, rows: resignations } = await db.Resignation.findAndCountAll({ where, include: [ { model: db.Outlet, as: 'outlet' }, { model: db.User, as: 'dealer', attributes: ['id', 'fullName', 'email', 'mobileNumber'] } ], order: [['createdAt', 'DESC']], limit: parseInt(limit), offset: offset }); res.json({ success: true, resignations, pagination: { total: count, page: parseInt(page), pages: Math.ceil(count / parseInt(limit)), limit: parseInt(limit) } }); } catch (error) { logger.error('Error fetching resignations:', error); next(error); } }; // Get resignation details export const getResignationById = async (req: AuthRequest, res: Response, next: NextFunction) => { try { if (!req.user) throw new Error('Unauthorized'); const { id } = req.params; const resignation = await db.Resignation.findOne({ where: { id }, include: [ { model: db.Outlet, as: 'outlet', include: [ { model: db.User, as: 'dealer', attributes: ['id', 'fullName', 'email', 'mobileNumber'] } ] }, { model: db.Worknote, as: 'worknotes' } ], order: [[{ model: db.Worknote, as: 'worknotes' }, 'createdAt', 'DESC']] }); if (!resignation) { return res.status(404).json({ success: false, message: 'Resignation not found' }); } // Check access permissions if (req.user.role === ROLES.DEALER && resignation.dealerId !== req.user.id) { return res.status(403).json({ success: false, message: 'Access denied' }); } res.json({ success: true, resignation }); } catch (error) { logger.error('Error fetching resignation details:', 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 { remarks } = req.body; const resignation = await db.Resignation.findByPk(id, { include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } // Determine next stage based on current stage 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.SPARES_CLEARANCE, [RESIGNATION_STAGES.SPARES_CLEARANCE]: RESIGNATION_STAGES.SERVICE_CLEARANCE, [RESIGNATION_STAGES.SERVICE_CLEARANCE]: RESIGNATION_STAGES.ACCOUNTS_CLEARANCE, [RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: RESIGNATION_STAGES.FINANCE, [RESIGNATION_STAGES.FINANCE]: 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' }); } // Update resignation const timeline = [...resignation.timeline, { stage: nextStage, timestamp: new Date(), user: req.user.fullName, action: 'Approved', remarks }]; await resignation.update({ currentStage: nextStage as any, status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`, progressPercentage: calculateProgress(nextStage), timeline }, { transaction }); // If completed, update outlet status and sync with SAP if (nextStage === RESIGNATION_STAGES.COMPLETED) { await (resignation as any).outlet.update({ status: 'Closed' }, { transaction }); // Trigger Mock SAP Sync ExternalMocksService.mockSyncDealerStatusToSap((resignation as any).outlet.code, 'Inactive') .catch(err => logger.error('Error syncing resignation completion to SAP:', err)); } // If F&F Initiated, create F&F record and fetch mock SAP dues if (nextStage === RESIGNATION_STAGES.FNF_INITIATED) { 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 }); // Create initial line items from SAP data 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 }); logger.info(`F&F record and mock line items created for resignation: ${resignation.resignationId}`); } // Create audit log await db.AuditLog.create({ userId: req.user.id, action: AUDIT_ACTIONS.APPROVED, entityType: 'resignation', entityId: resignation.id }, { transaction }); await transaction.commit(); logger.info(`Resignation ${resignation.resignationId} approved to ${nextStage} by ${req.user.email}`); 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 { reason } = req.body; if (!reason) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Rejection reason is required' }); } const resignation = await db.Resignation.findByPk(id, { include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } // Update resignation const timeline = [...resignation.timeline, { stage: 'Rejected', timestamp: new Date(), user: req.user.fullName, action: 'Rejected', reason }]; await resignation.update({ currentStage: RESIGNATION_STAGES.REJECTED, status: 'Rejected', progressPercentage: 0, rejectionReason: reason, timeline }, { transaction }); // Update outlet status back to Active await (resignation as any).outlet.update({ status: 'Active' }, { transaction }); // Create audit log await db.AuditLog.create({ userId: req.user.id, action: AUDIT_ACTIONS.REJECTED, entityType: 'resignation', entityId: resignation.id }, { transaction }); await transaction.commit(); logger.info(`Resignation ${resignation.resignationId} rejected by ${req.user.email}`); res.json({ success: true, message: 'Resignation rejected', resignation }); } catch (error) { if (transaction) await transaction.rollback(); logger.error('Error rejecting 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 { department, cleared, remarks } = req.body; const resignation = await db.Resignation.findByPk(id); 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); } };