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, APPLICATION_STATUS } from '../../common/config/constants.js'; import { WorkflowService } from '../../services/WorkflowService.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: 2, approvalMode: 'ROLE_MANDATORY', requiredRoles: ['DD Head', 'NBH'], isActive: true } }); // If policy already exists but has Finance, update it if (policy && Array.isArray(policy.requiredRoles) && policy.requiredRoles.includes('Finance')) { await policy.update({ requiredRoles: ['DD Head', 'NBH'], minApprovals: 2 }); } return policy; }; export const getRequest = async (req: Request, res: Response) => { try { const { applicationId } = req.params; const targetId = applicationId as string; 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(targetId); // Resolve application first to get UUID const application = await db.Application.findOne({ where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId } }); if (!application) { return res.status(404).json({ success: false, message: 'Application not found' }); } const request = await LoiRequest.findOne({ where: { applicationId: application.id }, 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' }); const application = await db.Application.findByPk(request.applicationId); if (application) { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.DEALER_CODE_GENERATION, req.user?.id || null, { reason: 'LOI Acknowledged by applicant', progressPercentage: 90 }); } 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 targetId = applicationId as string; 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(targetId); const application = await db.Application.findOne({ where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId } }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); const [request, created] = await LoiRequest.findOrCreate({ where: { applicationId: application.id }, defaults: { requestedBy: req.user?.id, status: 'In Progress' } }); // Initialize first level approval (DD Head) if not already exists await LoiApproval.findOrCreate({ where: { requestId: request.id, level: 1 }, defaults: { approverRole: 'DD Head', action: 'Pending' } }); await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_IN_PROGRESS, req.user?.id || null, { reason: 'LOI Request initiated for DD Head approval', progressPercentage: 75 }); res.status(201).json({ success: true, message: 'LOI Request initiated for DD Head 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.OnboardingDocument.count({ where: { applicationId: request.applicationId } }); 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' }); const application = await db.Application.findByPk(request.applicationId); if (application) { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_REJECTED, req.user.id, { reason: 'LOI Request rejected during approval', stage: 'Rejected', progressPercentage: 75 }); } 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`; const docRecord = await db.OnboardingDocument.create({ applicationId: request.applicationId, documentType: 'LOI', fileName: mockFile, filePath: `/uploads/loi/${mockFile}`, status: 'active' }); await LoiDocumentGenerated.create({ requestId: request.id, documentId: docRecord.id, version: '1.0' }); // Create Initial Security Deposit record (Advance Payment) await db.SecurityDeposit.findOrCreate({ where: { applicationId: request.applicationId, depositType: 'INITIAL' }, defaults: { amount: 200000, // 2 Lakhs Advance status: 'Pending' } }); const application = await db.Application.findByPk(request.applicationId); if (application) { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.SECURITY_DETAILS, req.user.id, { reason: 'LOI Request fully approved and document generated', progressPercentage: 80 }); } 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 targetId = applicationId as string; 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(targetId); const application = await db.Application.findOne({ where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId } }); if (!application) { return res.status(404).json({ success: false, message: 'Application not found' }); } const policy = await ensureLoiPolicy(); const actions = await StageApprovalAction.findAll({ where: { applicationId: application.id, 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 reqRecord = await LoiRequest.findByPk(requestId); let docId = null; if (reqRecord) { const docRecord = await db.OnboardingDocument.create({ applicationId: reqRecord.applicationId, documentType: 'LOI', fileName: mockFile, filePath: `/uploads/loi/${mockFile}`, status: 'active' }); docId = docRecord.id; } const doc = docId ? await LoiDocumentGenerated.create({ requestId, documentId: docId, version: '1.0' }) : null; // Bridge: Transition from LOI Issued -> Dealer Code Generation const request = await LoiRequest.findByPk(requestId); if (request) { const application = await db.Application.findByPk(request.applicationId); if (application) { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.DEALER_CODE_GENERATION, req.user?.id || null, { reason: 'LOI Document issued. Proceeding to Dealer Code Generation.', progressPercentage: 85 }); } } res.json({ success: true, message: 'LOI Document generated (Mock)', data: doc }); } catch (error) { res.status(500).json({ success: false, message: 'Error generating document' }); } };