290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import db from '../../database/models/index.js';
|
|
const { LoiRequest, LoiApproval, LoiDocumentGenerated, LoiAcknowledgement, AuditLog, StageApprovalPolicy, StageApprovalAction, User } = db;
|
|
import { AuthRequest } from '../../types/express.types.js';
|
|
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
|
|
|
const LOI_STAGE_CODE = 'LOI_APPROVAL';
|
|
|
|
const ensureLoiPolicy = async () => {
|
|
const [policy] = await StageApprovalPolicy.findOrCreate({
|
|
where: { stageCode: LOI_STAGE_CODE },
|
|
defaults: {
|
|
stageCode: LOI_STAGE_CODE,
|
|
minApprovals: 3,
|
|
approvalMode: 'ROLE_MANDATORY',
|
|
requiredRoles: ['Finance', 'DD Head', 'NBH'],
|
|
isActive: true
|
|
}
|
|
});
|
|
return policy;
|
|
};
|
|
|
|
export const getRequest = async (req: Request, res: Response) => {
|
|
try {
|
|
const { applicationId } = req.params;
|
|
const request = await LoiRequest.findOne({
|
|
where: { applicationId },
|
|
include: [
|
|
{ model: LoiApproval, as: 'approvals' },
|
|
{ model: LoiDocumentGenerated, as: 'generatedDocuments' },
|
|
{ model: LoiAcknowledgement, as: 'acknowledgement' }
|
|
]
|
|
});
|
|
res.json({ success: true, data: request });
|
|
} catch (error) {
|
|
console.error('Get LOI request error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching LOI request' });
|
|
}
|
|
};
|
|
|
|
export const acknowledgeRequest = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { requestId } = req.params;
|
|
const { documentId } = req.body;
|
|
|
|
const request = await LoiRequest.findByPk(requestId);
|
|
if (!request) return res.status(404).json({ success: false, message: 'LOI Request not found' });
|
|
|
|
await LoiAcknowledgement.create({
|
|
requestId,
|
|
applicationId: request.applicationId,
|
|
documentId,
|
|
acknowledgedAt: new Date(),
|
|
status: 'Acknowledged'
|
|
});
|
|
|
|
await db.Application.update({
|
|
overallStatus: 'Dealer Code Generation',
|
|
progressPercentage: 90
|
|
}, { where: { id: request.applicationId } });
|
|
|
|
res.json({ success: true, message: 'LOI Acknowledged by applicant' });
|
|
} catch (error) {
|
|
console.error('LOI Acknowledge error:', error);
|
|
res.status(500).json({ success: false, message: 'Error acknowledging LOI' });
|
|
}
|
|
};
|
|
|
|
export const createRequest = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { applicationId } = req.body;
|
|
|
|
const application = await db.Application.findByPk(applicationId);
|
|
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
|
|
|
|
const [request, created] = await LoiRequest.findOrCreate({
|
|
where: { applicationId },
|
|
defaults: {
|
|
requestedBy: req.user?.id,
|
|
status: 'In Progress'
|
|
}
|
|
});
|
|
|
|
// Initialize first level approval (Finance) if not already exists
|
|
await LoiApproval.findOrCreate({
|
|
where: { requestId: request.id, level: 1 },
|
|
defaults: {
|
|
approverRole: 'Finance',
|
|
action: 'Pending'
|
|
}
|
|
});
|
|
|
|
await application.update({
|
|
overallStatus: 'LOI In Progress',
|
|
progressPercentage: 75
|
|
});
|
|
|
|
res.status(201).json({ success: true, message: 'LOI Request initiated with Finance approval', data: request });
|
|
} catch (error) {
|
|
console.error('Create LOI request error:', error);
|
|
res.status(500).json({ success: false, message: 'Error creating LOI request' });
|
|
}
|
|
};
|
|
|
|
export const approveRequest = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
if (!req.user?.id || !req.user?.roleCode) {
|
|
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
|
}
|
|
const { requestId } = req.params;
|
|
const { action, remarks } = req.body; // action: Approved/Rejected
|
|
|
|
const request = await LoiRequest.findByPk(requestId);
|
|
if (!request) return res.status(404).json({ success: false, message: 'LOI Request not found' });
|
|
const policy = await ensureLoiPolicy();
|
|
const requiredRoles: string[] = Array.isArray(policy.requiredRoles) ? policy.requiredRoles : [];
|
|
|
|
if (requiredRoles.length > 0 && !requiredRoles.includes(req.user.roleCode) && req.user.roleCode !== 'Super Admin') {
|
|
return res.status(403).json({
|
|
success: false,
|
|
message: `Role ${req.user.roleCode} is not allowed to approve ${LOI_STAGE_CODE}`
|
|
});
|
|
}
|
|
|
|
// Find current pending approval
|
|
const currentApproval = await LoiApproval.findOne({
|
|
where: { requestId, action: 'Pending' },
|
|
order: [['level', 'ASC']]
|
|
});
|
|
|
|
if (!currentApproval) {
|
|
return res.status(400).json({ success: false, message: 'No pending approval levels found' });
|
|
}
|
|
|
|
// MANDATORY DOCUMENT CHECK (SRS Requirement)
|
|
// Level 2+ requires minimum set of documents uploaded by applicant
|
|
if (currentApproval.level === 1 && action === 'Approved') {
|
|
const docCount = await db.Document.count({
|
|
where: { requestId: request.applicationId, requestType: 'application' }
|
|
});
|
|
if (docCount < 5) { // SRS requires 18, using 5 for functional demo
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: `Mandatory Document Check Failed: Applicant must upload at least 5 required documents (CIBIL, City Map, etc.) before DD Head approval. Current: ${docCount}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// 1. Update current level
|
|
await currentApproval.update({
|
|
action,
|
|
remarks,
|
|
approverId: req.user.id,
|
|
approvedAt: action === 'Approved' ? new Date() : null
|
|
});
|
|
|
|
const normalizedDecision = action === 'Rejected' ? 'Rejected' : 'Approved';
|
|
await StageApprovalAction.upsert({
|
|
applicationId: request.applicationId,
|
|
interviewId: null,
|
|
stageCode: LOI_STAGE_CODE,
|
|
actorUserId: req.user.id,
|
|
actorRole: req.user.roleCode,
|
|
decision: normalizedDecision,
|
|
remarks: remarks || null
|
|
});
|
|
|
|
const stageActions = await StageApprovalAction.findAll({
|
|
where: { applicationId: request.applicationId, stageCode: LOI_STAGE_CODE }
|
|
});
|
|
const approvedRoles = new Set(
|
|
stageActions.filter((a: any) => a.decision === 'Approved').map((a: any) => a.actorRole)
|
|
);
|
|
const hasRejection = stageActions.some((a: any) => a.decision === 'Rejected');
|
|
|
|
const hasAllRequiredRoleApprovals = requiredRoles.length === 0
|
|
? true
|
|
: requiredRoles.every((role: string) => approvedRoles.has(role));
|
|
|
|
const meetsMinApprovals = approvedRoles.size >= (policy.minApprovals || 1);
|
|
|
|
// 2. Handle Logic based on Action
|
|
if (action === 'Rejected' || hasRejection) {
|
|
await request.update({ status: 'Rejected' });
|
|
await db.Application.update({
|
|
overallStatus: 'LOI Rejected',
|
|
currentStage: 'Rejected',
|
|
progressPercentage: 75
|
|
}, { where: { id: request.applicationId } });
|
|
return res.json({ success: true, message: 'LOI Request rejected' });
|
|
}
|
|
|
|
if (hasAllRequiredRoleApprovals && meetsMinApprovals) {
|
|
// Final Approval reached
|
|
await request.update({ status: 'Approved', approvedBy: req.user.id, approvedAt: new Date() });
|
|
|
|
// Trigger Mock Document Generation
|
|
const mockFile = `LOI_${request.id}.pdf`;
|
|
await LoiDocumentGenerated.create({
|
|
requestId: request.id,
|
|
documentType: 'LOI',
|
|
fileName: mockFile,
|
|
filePath: `/uploads/loi/${mockFile}`
|
|
});
|
|
|
|
await db.Application.update({
|
|
overallStatus: 'Security Details',
|
|
progressPercentage: 80
|
|
}, { where: { id: request.applicationId } });
|
|
|
|
res.json({ success: true, message: 'LOI Request fully approved and document generated' });
|
|
} else {
|
|
res.json({
|
|
success: true,
|
|
message: 'Approval recorded. Waiting for remaining required approvers.',
|
|
data: {
|
|
stageCode: LOI_STAGE_CODE,
|
|
requiredRoles,
|
|
minApprovals: policy.minApprovals,
|
|
approvedRoles: Array.from(approvedRoles),
|
|
hasAllRequiredRoleApprovals,
|
|
meetsMinApprovals
|
|
}
|
|
});
|
|
}
|
|
|
|
await AuditLog.create({
|
|
userId: req.user?.id,
|
|
action: action === 'Approved' ? AUDIT_ACTIONS.LOI_APPROVED : AUDIT_ACTIONS.LOI_REJECTED,
|
|
entityType: 'loi_request',
|
|
entityId: requestId,
|
|
newData: { level: currentApproval.level, action }
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Approve LOI request error:', error);
|
|
res.status(500).json({ success: false, message: 'Error processing approval' });
|
|
}
|
|
};
|
|
|
|
export const getApprovalStatus = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { applicationId } = req.params;
|
|
const policy = await ensureLoiPolicy();
|
|
const actions = await StageApprovalAction.findAll({
|
|
where: { applicationId, stageCode: LOI_STAGE_CODE },
|
|
include: [{ model: User, as: 'actor', attributes: ['id', 'fullName', 'email', 'roleCode'] }],
|
|
order: [['updatedAt', 'DESC']]
|
|
});
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
stageCode: LOI_STAGE_CODE,
|
|
minApprovals: policy.minApprovals,
|
|
approvalMode: policy.approvalMode,
|
|
requiredRoles: policy.requiredRoles || [],
|
|
actions
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Get LOI approval status error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching LOI approval status' });
|
|
}
|
|
};
|
|
|
|
export const generateDocument = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { requestId } = req.body;
|
|
// Mocking document generation
|
|
const mockFile = `LOI_MANUAL_${Date.now()}.pdf`;
|
|
const doc = await LoiDocumentGenerated.create({
|
|
requestId,
|
|
documentType: 'LOI',
|
|
fileName: mockFile,
|
|
filePath: `/uploads/loi/${mockFile}`
|
|
});
|
|
|
|
await AuditLog.create({
|
|
userId: req.user?.id,
|
|
action: AUDIT_ACTIONS.LOI_GENERATED,
|
|
entityType: 'loi_request',
|
|
entityId: requestId
|
|
});
|
|
|
|
res.json({ success: true, message: 'LOI Document generated (Mock)', data: doc });
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, message: 'Error generating document' });
|
|
}
|
|
};
|