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

488 lines
16 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';
// Generate unique resignation ID
const generateResignationId = async (): Promise<string> => {
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<string, number> = {
[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<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.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);
}
};