383 lines
17 KiB
TypeScript
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);
|
|
}
|
|
};
|