Dealer_Onboarding_Backend/src/modules/self-service/resignation.controller.ts

383 lines
17 KiB
TypeScript

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<string> => {
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<string, string> = {
[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<string, string> = {
[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);
}
};