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