Dealer_Onboarding_Backend/src/modules/loi/loi.controller.ts

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' });
}
};