diff --git a/check_bug_app.ts b/check_bug_app.ts new file mode 100644 index 0000000..21b89a9 --- /dev/null +++ b/check_bug_app.ts @@ -0,0 +1,18 @@ +import db from './src/database/models/index.js'; + +async function check() { + const app = await db.Application.findOne({ where: { applicationId: 'APP-2026-2DC97C' } }); + if (!app) { + console.log('Application APP-2026-2DC97C not found'); + return; + } + console.log(`Application: APP-2026-2DC97C, id: ${app.id}, Status: ${app.overallStatus}, Stage: ${app.currentStage}, Progress: ${app.progressPercentage}`); + + const progress = await db.ApplicationProgress.findAll({ where: { applicationId: app.id } }); + console.log('--- Application Progress ---'); + progress.forEach(p => console.log(`Stage: ${p.stageName}, Status: ${p.status}`)); + + process.exit(0); +} + +check(); diff --git a/check_db.ts b/check_db.ts new file mode 100644 index 0000000..291b971 --- /dev/null +++ b/check_db.ts @@ -0,0 +1,20 @@ +import db from './src/database/models/index.js'; + +async function check() { + const app = await db.Application.findOne({ where: { applicationId: 'APP-TEST-001' } }); + if (!app) { + console.log('Application APP-TEST-001 not found'); + return; + } + const deposits = await db.SecurityDeposit.findAll({ + where: { applicationId: app.id }, + include: [{ model: db.User, as: 'verifier', attributes: ['fullName'] }] + }); + console.log('--- Security Deposits for APP-TEST-001 ---'); + deposits.forEach(d => { + console.log(`ID: ${d.id}, Type: ${d.depositType}, Status: ${d.status}, Ref: ${d.paymentReference}, Verifier: ${d.verifier?.fullName || 'N/A'}`); + }); + process.exit(0); +} + +check(); diff --git a/check_final.ts b/check_final.ts new file mode 100644 index 0000000..ea0dfab --- /dev/null +++ b/check_final.ts @@ -0,0 +1,16 @@ +import db from './src/database/models/index.js'; + +async function check() { + const app = await db.Application.findOne({ where: { applicationId: 'APP-2026-D444A1' } }); + if (!app) { + console.log('Application not found'); + return; + } + const finalDeposit = await db.SecurityDeposit.findOne({ + where: { applicationId: app.id, depositType: 'FINAL' } + }); + console.log(`Final Deposit Status: ${finalDeposit?.status || 'NOT FOUND'}`); + process.exit(0); +} + +check(); diff --git a/check_loi.ts b/check_loi.ts new file mode 100644 index 0000000..c2a08fe --- /dev/null +++ b/check_loi.ts @@ -0,0 +1,25 @@ +import db from './src/database/models/index.js'; + +async function check() { + const app = await db.Application.findOne({ where: { applicationId: 'APP-TEST-001' } }); + if (!app) { + console.log('Application APP-TEST-001 not found'); + return; + } + console.log(`Application Status: ${app.overallStatus}, Stage: ${app.currentStage}`); + + const loiReq = await db.LoiRequest.findOne({ where: { applicationId: app.id } }); + if (!loiReq) { + console.log('LoiRequest not found for this application'); + } else { + console.log(`LoiRequest Status: ${loiReq.status}`); + const financeApproval = await db.LoiApproval.findOne({ + where: { requestId: loiReq.id, approverRole: 'Finance' } + }); + console.log(`Finance Approval Action: ${financeApproval?.action || 'No record found'}`); + } + + process.exit(0); +} + +check(); diff --git a/check_new_app.ts b/check_new_app.ts new file mode 100644 index 0000000..ccd4a72 --- /dev/null +++ b/check_new_app.ts @@ -0,0 +1,21 @@ +import db from './src/database/models/index.js'; + +async function check() { + const app = await db.Application.findOne({ where: { applicationId: 'APP-2026-D444A1' } }); + if (!app) { + console.log('Application APP-2026-D444A1 not found'); + return; + } + console.log(`Application: APP-2026-D444A1, id: ${app.id}, Status: ${app.overallStatus}, Stage: ${app.currentStage}`); + + const progress = await db.ApplicationProgress.findAll({ where: { applicationId: app.id } }); + console.log('--- Application Progress ---'); + progress.forEach(p => console.log(`Stage: ${p.stageName}, Status: ${p.status}`)); + + const loiReq = await db.LoiRequest.findOne({ where: { applicationId: app.id } }); + console.log(`LoiRequest: ${loiReq ? loiReq.status : 'NOT FOUND'}`); + + process.exit(0); +} + +check(); diff --git a/repair_fdd_stage.ts b/repair_fdd_stage.ts new file mode 100644 index 0000000..5359c3b --- /dev/null +++ b/repair_fdd_stage.ts @@ -0,0 +1,15 @@ +import db from './src/database/models/index.js'; + +async function repair() { + const app = await db.Application.findOne({ where: { applicationId: 'APP-2026-2DC97C' } }); + if (app) { + await app.update({ + currentStage: 'FDD', + overallStatus: 'FDD Verification' // ensure it's synced + }); + console.log(`Repaired APP-2026-2DC97C: Stage set to FDD`); + } + process.exit(0); +} + +repair(); diff --git a/repair_loi.ts b/repair_loi.ts new file mode 100644 index 0000000..dcacc7d --- /dev/null +++ b/repair_loi.ts @@ -0,0 +1,35 @@ +import db from './src/database/models/index.js'; +import { APPLICATION_STATUS } from './src/common/config/constants.js'; +import { syncApplicationProgress } from './src/common/utils/progress.js'; + +async function repair() { + const app = await db.Application.findOne({ where: { applicationId: 'APP-2026-D444A1' } }); + if (!app) { + console.log('Application not found'); + return; + } + + // 1. Ensure LoiRequest exists + const [request, created] = await db.LoiRequest.findOrCreate({ + where: { applicationId: app.id }, + defaults: { + requestedBy: null, // System initiated via FDD completion + status: 'In Progress' + } + }); + console.log(`LoiRequest ${created ? 'Created' : 'Found'}`); + + // 2. Ensure Finance Approval entry exists in LOI + await db.LoiApproval.findOrCreate({ + where: { requestId: request.id, approverRole: 'Finance' }, + defaults: { action: 'Pending', level: 1 } + }); + + // 3. Sync Progress Record (This creates the missing LOI Approval progress item) + await syncApplicationProgress(app.id, APPLICATION_STATUS.LOI_IN_PROGRESS); + console.log('Progress records synchronized.'); + + process.exit(0); +} + +repair(); diff --git a/reset_apps.ts b/reset_apps.ts new file mode 100644 index 0000000..f897c21 --- /dev/null +++ b/reset_apps.ts @@ -0,0 +1,64 @@ +import db from './src/database/models/index.js'; +import { APPLICATION_STATUS, APPLICATION_STAGES } from './src/common/config/constants.js'; + +async function reset() { + // List of application IDs provided in previous queries + const appIds = ['APP-2026-D444A1', 'APP-2026-2DC97C']; + + for (const id of appIds) { + const app = await db.Application.findOne({ where: { applicationId: id } }); + if (!app) continue; + + console.log(`Resetting application ${id}...`); + + // 1. Delete future stage records + const loiReq = await db.LoiRequest.findOne({ where: { applicationId: app.id } }); + if (loiReq) { + await db.LoiApproval.destroy({ where: { requestId: loiReq.id } }); + await db.LoiDocumentGenerated.destroy({ where: { requestId: loiReq.id } }); + await loiReq.destroy(); + } + + const loaReq = await db.LoaRequest.findOne({ where: { applicationId: app.id } }); + if (loaReq) { + await db.LoaApproval.destroy({ where: { requestId: loaReq.id } }); + await loaReq.destroy(); + } + + // 2. Delete FDD Reports and reset assignment + const assignments = await db.FddAssignment.findAll({ where: { applicationId: app.id } }); + for (const ass of assignments) { + await db.FddReport.destroy({ where: { assignmentId: ass.id } }); + await ass.update({ status: 'Assigned' }); + } + + // 3. Reset Application status + await app.update({ + overallStatus: 'FDD Verification', + currentStage: 'FDD', + progressPercentage: 70 + }); + + // 4. Reset Progress Tracker + await db.ApplicationProgress.destroy({ + where: { + applicationId: app.id, + stageName: ['LOI Approval', 'Security Details', 'LOI Issue', 'Dealer Code Generation', 'Architecture Team Assigned', 'Statutory GST'] + } + }); + + await db.ApplicationProgress.upsert({ + applicationId: app.id, + stageName: 'FDD', + stageOrder: 8, + status: 'active', + completionPercentage: 70 + }); + + console.log(`Application ${id} is now back to FDD stage.`); + } + + process.exit(0); +} + +reset(); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 78495a0..c046d89 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -44,6 +44,10 @@ export const APPLICATION_STAGES = { LEGAL: 'Legal', ARCHITECTURE: 'Architecture Team', FINANCE: 'Finance', + FDD: 'FDD', + LOI: 'LOI', + LOA: 'LOA', + EOR: 'EOR', LEVEL_1_APPROVED: 'Level 1 Approved', LEVEL_2_APPROVED: 'Level 2 Approved', LEVEL_2_RECOMMENDED: 'Level 2 Recommended', @@ -96,7 +100,8 @@ export const APPLICATION_STATUS = { INAUGURATION: 'Inauguration', ONBOARDED: 'Onboarded', DISQUALIFIED: 'Disqualified', - LOI_REJECTED: 'LOI Rejected' + LOI_REJECTED: 'LOI Rejected', + RETURNED_TO_FDD: 'Returned to FDD' } as const; // Termination Stages @@ -389,6 +394,8 @@ export const DOCUMENT_TYPES = { RELOCATION_BUILDING_PLAN: 'Building plan approval', RELOCATION_ELECTRICITY_DOCS: 'Electricity connection documents', RELOCATION_WATER_DOCS: 'Water supply documents', + INCOME_TAX_RETURNS: 'Income Tax Returns (ITR)', + BUSINESS_VALUATION_REPORT: 'Business Valuation Report', OTHER: 'Other' } as const; diff --git a/src/common/utils/progress.ts b/src/common/utils/progress.ts index cc1ec1c..d13c7fb 100644 --- a/src/common/utils/progress.ts +++ b/src/common/utils/progress.ts @@ -120,6 +120,14 @@ export const syncApplicationProgress = async (applicationId: string, overallStat await updateApplicationProgress(applicationId, currentStageName, isCompleted ? 'completed' : 'active', isCompleted ? 100 : 50); + + // AUTO-INITIATE NEXT STAGE: If this stage is completed, mark the next one as 'active' (Pending) + if (isCompleted) { + const nextStage = ONBOARDING_STAGES.find(s => s.order === stage.order + 1); + if (nextStage) { + await updateApplicationProgress(applicationId, nextStage.name, 'pending', 0); + } + } } } }; diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index cfded1f..3b55348 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -272,6 +272,7 @@ export default (sequelize: Sequelize) => { Application.hasOne(models.Dealer, { foreignKey: 'applicationId', as: 'dealer' }); Application.hasMany(models.SecurityDeposit, { foreignKey: 'applicationId', as: 'securityDeposits' }); Application.hasOne(models.EorChecklist, { foreignKey: 'applicationId', as: 'eorChecklist' }); + Application.hasMany(models.FddAssignment, { foreignKey: 'applicationId', as: 'fddAssignments' }); }; return Application; diff --git a/src/modules/assessment/assessment.controller.ts b/src/modules/assessment/assessment.controller.ts index 6b1457b..0f632dd 100644 --- a/src/modules/assessment/assessment.controller.ts +++ b/src/modules/assessment/assessment.controller.ts @@ -53,9 +53,10 @@ const processStageDecision = async (params: { roleCode: string; interviewId?: string; nextStatus?: string; + nextStage?: string; nextProgress?: number; }) => { - const { applicationId, stageCode, decision, remarks, userId, roleCode, interviewId, nextStatus, nextProgress } = params; + const { applicationId, stageCode, decision, remarks, userId, roleCode, interviewId, nextStatus, nextStage, nextProgress } = params; const policy = await db.StageApprovalPolicy.findOne({ where: { stageCode } }); if (!policy) return { noPolicy: true }; @@ -132,14 +133,32 @@ const processStageDecision = async (params: { }); statusUpdated = true; } - } else if (evaluation.policyMet && nextStatus) { + } else if (evaluation.policyMet) { const application = await db.Application.findByPk(applicationId); if (application) { - await WorkflowService.transitionApplication(application, nextStatus, userId, { - reason: `Policy met for ${stageCode}`, - progressPercentage: nextProgress - }); - statusUpdated = true; + let targetStatus = nextStatus; + let targetStage = nextStage; + let targetProgress = nextProgress; + + // Sequential Override: Ensure one-by-one progression + if (stageCode === 'LOI_APPROVAL') { + targetStatus = APPLICATION_STATUS.SECURITY_DETAILS; + targetStage = APPLICATION_STAGES.LOI; + targetProgress = 75; + } else if (stageCode === 'LOA_APPROVAL') { + targetStatus = APPLICATION_STATUS.LOA_ISSUED; + targetStage = APPLICATION_STAGES.LOA; + targetProgress = 95; + } + + if (targetStatus) { + await WorkflowService.transitionApplication(application, targetStatus, userId, { + reason: `Policy satisfied for ${stageCode}. Moving to next sequential step.`, + stage: targetStage, + progressPercentage: targetProgress + }); + statusUpdated = true; + } } } @@ -171,9 +190,10 @@ const processInterviewApprovalDecision = async (params: { // Ensure policy exists for interviews await ensureInterviewPolicy(interview.level); - const nextStatusMap: any = { 1: 'Level 1 Approved', 2: 'Level 2 Approved', 3: 'FDD Verification' }; + const nextStatusMap: any = { 1: 'Level 1 Approved', 2: 'Level 2 Approved', 3: 'Level 3 Approved' }; + const nextStageMap: any = { 1: APPLICATION_STAGES.LEVEL_1_APPROVED, 2: APPLICATION_STAGES.LEVEL_2_APPROVED, 3: APPLICATION_STAGES.FDD }; const progressMap: any = { 1: 40, 2: 55, 3: 70 }; - + const result = await processStageDecision({ applicationId: interview.applicationId, stageCode, @@ -183,6 +203,7 @@ const processInterviewApprovalDecision = async (params: { roleCode, interviewId, nextStatus: nextStatusMap[interview.level] || 'Approved', + nextStage: nextStageMap[interview.level] || APPLICATION_STAGES.APPROVED, nextProgress: progressMap[interview.level] }); diff --git a/src/modules/fdd/fdd.controller.ts b/src/modules/fdd/fdd.controller.ts index 36815b7..d7c3e6d 100644 --- a/src/modules/fdd/fdd.controller.ts +++ b/src/modules/fdd/fdd.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; const { FddAssignment, FddReport, AuditLog, Application } = db; import { AuthRequest } from '../../types/express.types.js'; -import { AUDIT_ACTIONS, APPLICATION_STATUS } from '../../common/config/constants.js'; +import { AUDIT_ACTIONS, APPLICATION_STATUS, APPLICATION_STAGES } from '../../common/config/constants.js'; import { WorkflowService } from '../../services/WorkflowService.js'; export const getAssignment = async (req: Request, res: Response) => { @@ -29,9 +29,19 @@ export const assignAgency = async (req: AuthRequest, res: Response) => { status: 'Assigned' }); + // Bridge: Transition application to active FDD stage + const application = await Application.findByPk(applicationId); + if (application) { + await WorkflowService.transitionApplication(application, APPLICATION_STATUS.FDD_VERIFICATION, req.user?.id || null, { + reason: 'FDD Agency assigned. Initiating financial due diligence.', + stage: APPLICATION_STAGES.FDD, + progressPercentage: 70 + }); + } + await AuditLog.create({ userId: req.user?.id, - action: AUDIT_ACTIONS.CREATED, + action: AUDIT_ACTIONS.FDD_ASSIGNED, entityType: 'fdd_assignment', entityId: assignment.id }); @@ -67,10 +77,61 @@ export const uploadReport = async (req: AuthRequest, res: Response) => { if (assignmentRecord) { const application = await Application.findByPk(assignmentRecord.applicationId); if (application) { - await WorkflowService.transitionApplication(application, APPLICATION_STATUS.SECURITY_DETAILS, req.user?.id || null, { - reason: 'FDD Report submitted by agency', - progressPercentage: 70 + // Ensure LOI Request exists for the next stage + const [loiRequest] = await db.LoiRequest.findOrCreate({ + where: { applicationId: application.id }, + defaults: { + requestedBy: req.user?.id, + status: 'In Progress' + } }); + + // Pre-initialize Finance approval for LOI stage + await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_IN_PROGRESS, req.user?.id || null, { + reason: 'FDD Report submitted and verified. Moving to LOI Approval stage.', + stage: APPLICATION_STAGES.LOI, + progressPercentage: 65 + }); + + // Bridge 2.0: Automatically initialize LOI Records so the Initial Payment auto-approval finds them + console.log(`[DEBUG] Initializing LOI Records for Application: ${application.id}`); + const [loiReq] = await db.LoiRequest.findOrCreate({ + where: { applicationId: application.id }, + defaults: { status: 'Pending Approval' } + }); + console.log(`[DEBUG] LOI Request ID: ${loiReq.id}, Overall Status: ${loiReq.status}`); + + const roles = ['Finance', 'DD Head', 'NBH']; + await Promise.all(roles.map(async (role) => { + let action = 'Pending'; + let comments = null; + + if (role === 'Finance') { + console.log(`[DEBUG] Checking for existing verified INITIAL deposit for ${application.id}`); + const verifiedDeposit = await db.SecurityDeposit.findOne({ + where: { applicationId: application.id, depositType: 'INITIAL', status: 'Verified' } + }); + if (verifiedDeposit) { + console.log(`[DEBUG] FOUND VERIFIED DEPOSIT! Auto-approving Finance role in LOI.`); + action = 'Approved'; + comments = 'Auto-approved: Initial Security Deposit already verified.'; + } else { + console.log(`[DEBUG] NO Verified INITIAL deposit found during FDD upload.`); + } + } + + const [approval, created] = await db.LoiApproval.findOrCreate({ + where: { requestId: loiReq.id, approverRole: role }, + defaults: { action, comments, level: 1 } + }); + console.log(`[DEBUG] Role ${role}: Status=${approval.action} (Created: ${created})`); + return approval; + })); + + // If Finance was auto-approved, trigger policy evaluation + console.log(`[DEBUG] Finalizing FDD Upload -> Evaluating Stage Policy for LOI_APPROVAL`); + const evalResult = await WorkflowService.evaluateStagePolicy(application.id, 'LOI_APPROVAL'); + console.log(`[DEBUG] Policy Met: ${evalResult.policyMet}, Approved Roles: ${Array.from(evalResult.approvedRoles || [])}`); } } diff --git a/src/modules/loa/loa.controller.ts b/src/modules/loa/loa.controller.ts index f6f34c3..947400d 100644 --- a/src/modules/loa/loa.controller.ts +++ b/src/modules/loa/loa.controller.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; const { LoaRequest, LoaApproval, LoaDocumentGenerated, SecurityDeposit, AuditLog, StageApprovalPolicy, StageApprovalAction, User } = db; import { AuthRequest } from '../../types/express.types.js'; -import { AUDIT_ACTIONS, APPLICATION_STATUS } from '../../common/config/constants.js'; +import { AUDIT_ACTIONS, APPLICATION_STATUS, APPLICATION_STAGES } from '../../common/config/constants.js'; import { WorkflowService } from '../../services/WorkflowService.js'; const LOA_STAGE_CODE = 'LOA_APPROVAL'; @@ -102,15 +102,20 @@ export const approveRequest = async (req: AuthRequest, res: Response) => { if (!currentApproval) return res.status(400).json({ success: false, message: 'No pending approval found' }); - // MANDATORY FINANCIAL CHECK + // MANDATORY FINANCIAL CHECK: LOA MUST HAVE FINAL SECURITY DEPOSIT VERIFIED if (action === 'Approved') { const finalDeposit = await SecurityDeposit.findOne({ - where: { applicationId: request.applicationId, depositType: 'FINAL', status: 'Verified' } + where: { + applicationId: request.applicationId, + depositType: 'FINAL', + status: 'Verified' + } }); + if (!finalDeposit) { return res.status(400).json({ success: false, - message: 'LOA Approval Blocked: Final Security Deposit (₹15L) must be verified by Finance team before proceeding.' + message: `LOA Approval Blocked: The Final Security Deposit (₹15L) is either Pending or not found. Finance team must verify the payment before proceeding.` }); } } @@ -258,8 +263,8 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) => if (deposit) { await deposit.update({ amount, paymentReference, proofDocumentId, status, - verifiedBy: status === 'Verified' ? req.user?.id : null, - verifiedAt: status === 'Verified' ? new Date() : null + verifiedBy: status === 'Verified' ? req.user?.id : deposit.verifiedBy, + verifiedAt: status === 'Verified' && !deposit.verifiedAt ? new Date() : deposit.verifiedAt }); } else { deposit = await SecurityDeposit.create({ @@ -268,73 +273,84 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) => paymentReference, proofDocumentId, status: status || 'Pending', - depositType: depositType || 'INITIAL' + depositType: depositType || 'INITIAL', + verifiedBy: status === 'Verified' ? req.user?.id : null, + verifiedAt: status === 'Verified' ? new Date() : null }); } - // AUTOMATION: If Initial Payment is Verified, auto-approve "Finance" role in LOI Stage - if (depositType === 'INITIAL' && status === 'Verified') { + // --- FETCH WITH JOIN FOR FRONTEND --- + const updatedDeposit = await SecurityDeposit.findByPk(deposit.id, { + include: [{ model: User, as: 'verifier', attributes: ['fullName'] }] + }); + + // --- AUTOMATION: After verification transitions --- + + // 1. If INITIAL Payment Verified -> Approve LOI Finance Role + // Bridge 1.0: AUTOMATED LOI APPROVAL IF INITIAL PAYMENT IS VERIFIED + console.log(`[DEBUG] Payment Verification Trace -> Deposit Type: ${depositType}, Status: ${status}`); + if ((depositType === 'INITIAL' || !depositType) && status === 'Verified') { + console.log(`[DEBUG] Initial Deposit VERIFIED for Application: ${application.id}. Ensuring LOI records exist...`); const LoiRequest = db.LoiRequest; const LoiApproval = db.LoiApproval; - const StageApprovalAction = db.StageApprovalAction; - const loiReq = await LoiRequest.findOne({ where: { applicationId: application.id } }); - if (loiReq) { - // 1. Update module-specific approval table - const financeApproval = await LoiApproval.findOne({ - where: { requestId: loiReq.id, approverRole: 'Finance', action: 'Pending' } - }); - if (financeApproval) { - await financeApproval.update({ - action: 'Approved', - remarks: 'Auto-approved via Finance payment verification', - approverId: req.user?.id, - approvedAt: new Date() - }); - } + // 1. Proactively ensure the LOI Request exists if the payment is cleared + const [loiReq, createdReq] = await db.LoiRequest.findOrCreate({ + where: { applicationId: application.id }, + defaults: { status: 'Pending Approval' } + }); + console.log(`[DEBUG] LOI Request ID: ${loiReq.id}, Status: ${loiReq.status} (Created: ${createdReq})`); - // 2. Update generic StageApprovalAction table - await StageApprovalAction.upsert({ - applicationId: application.id, - stageCode: 'LOI_APPROVAL', + // 2. Initialize the three required approval roles for the LOI step + const roles = ['Finance', 'DD Head', 'NBH']; + await Promise.all(roles.map(async (role) => { + const [approval, created] = await db.LoiApproval.findOrCreate({ + where: { requestId: loiReq.id, approverRole: role }, + defaults: { action: 'Pending', level: 1 } + }); + console.log(`[DEBUG] Role ${role}: Status=${approval.action} (Created: ${created})`); + })); + + // 3. Mark the Finance role as Approved based on this verified payment + const financeApproval = await db.LoiApproval.findOne({ + where: { requestId: loiReq.id, approverRole: 'Finance' } + }); + + if (financeApproval) { + console.log(`[DEBUG] Marking Finance Approval record as Approved...`); + await financeApproval.update({ + action: 'Approved', actorUserId: req.user?.id, - actorRole: 'Finance', - decision: 'Approved', - remarks: 'Auto-approved via Finance payment verification' + actionedAt: new Date(), + comments: 'Initial Security Deposit verified.' }); - // 3. Check if LOI can now be fully approved (copied logic from loi.controller) - const policy = await db.StageApprovalPolicy.findOne({ where: { stageCode: 'LOI_APPROVAL' } }); - const requiredRoles = policy?.requiredRoles || ['Finance', 'DD Head', 'NBH']; - - const stageActions = await StageApprovalAction.findAll({ - where: { applicationId: application.id, stageCode: 'LOI_APPROVAL' } + console.log(`[DEBUG] Initial Security Deposit verified. Transitioning to LOI Issued...`); + await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_ISSUED, req.user?.id || null, { + reason: 'Initial Security Deposit verified. Proceeding to LOI Issuance.', + stage: APPLICATION_STAGES.LOI, + progressPercentage: 80 }); - const approvedRoles = new Set(stageActions.filter((a: any) => a.decision === 'Approved').map((a: any) => a.actorRole)); - const meetsMinApprovals = approvedRoles.size >= (policy?.minApprovals || 3); - const hasAllRequired = requiredRoles.every((r: string) => approvedRoles.has(r)); - - if (hasAllRequired && meetsMinApprovals && loiReq.status !== 'Approved') { - await loiReq.update({ status: 'Approved', approvedBy: req.user?.id, approvedAt: new Date() }); - - const mockFile = `LOI_${loiReq.id}.pdf`; - await db.LoiDocumentGenerated.findOrCreate({ - where: { requestId: loiReq.id, documentType: 'LOI' }, - defaults: { - fileName: mockFile, - filePath: `/uploads/loi/${mockFile}` - } - }); - - await WorkflowService.transitionApplication(application, APPLICATION_STATUS.SECURITY_DETAILS, req.user?.id, { - reason: 'LOI Request fully approved via automated finance verification', - progressPercentage: 80 - }); - } + } else { + console.log(`[DEBUG] No pending Finance approval in LOI stage. Skipping auto-bridge.`); } } - res.json({ success: true, message: 'Security Deposit updated', data: deposit }); + // 2. If FINAL Payment Verified -> Move to LOA Pending stage + if (depositType === 'FINAL' && status === 'Verified') { + // Ensure LoaRequest exists for the next step + await db.LoaRequest.findOrCreate({ + where: { applicationId: application.id }, + defaults: { status: 'pending', requestedBy: req.user?.id } + }); + + await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOA_PENDING, req.user?.id || null, { + reason: 'Final Security Deposit Verified. Initiating LOA Approval stage.', + progressPercentage: 90 + }); + } + + res.json({ success: true, message: 'Security Deposit updated', data: updatedDeposit }); } catch (error) { console.error('Update Security Deposit error:', error); res.status(500).json({ success: false, message: 'Error updating security deposit' }); @@ -356,6 +372,7 @@ export const getSecurityDeposit = async (req: Request, res: Response) => { const deposits = await SecurityDeposit.findAll({ where: { applicationId: application.id }, + include: [{ model: User, as: 'verifier', attributes: ['fullName'] }], order: [['createdAt', 'ASC']] }); res.json({ success: true, data: deposits }); diff --git a/src/modules/loi/loi.controller.ts b/src/modules/loi/loi.controller.ts index b3c6d5d..33ba022 100644 --- a/src/modules/loi/loi.controller.ts +++ b/src/modules/loi/loi.controller.ts @@ -294,12 +294,17 @@ export const generateDocument = async (req: AuthRequest, res: Response) => { filePath: `/uploads/loi/${mockFile}` }); - await AuditLog.create({ - userId: req.user?.id, - action: AUDIT_ACTIONS.LOI_GENERATED, - entityType: 'loi_request', - entityId: requestId - }); + // 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) { diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index 3b79db6..18e4fed 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; -const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, State, Region, Zone, SecurityDeposit } = db; +const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, State, Region, Zone, SecurityDeposit, FddAssignment, FddReport, OnboardingDocument, Worknote, StageApprovalAction, DealerCode, Dealer, RequestParticipant, QuestionnaireResponse, QuestionnaireQuestion, QuestionnaireOption, User } = db; import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js'; import { v4 as uuidv4 } from 'uuid'; import { Op } from 'sequelize'; @@ -181,35 +181,57 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => { include: [ { model: ApplicationStatusHistory, as: 'statusHistory', separate: true, order: [['createdAt', 'DESC']] }, { model: ApplicationProgress, as: 'progressTracking', separate: true, order: [['stageOrder', 'ASC']] }, - { model: SecurityDeposit, as: 'securityDeposits' }, + { + model: SecurityDeposit, + as: 'securityDeposits', + include: [{ model: User, as: 'verifier', attributes: ['fullName'] }] + }, { - model: db.QuestionnaireResponse, + model: QuestionnaireResponse, as: 'questionnaireResponses', separate: true, include: [ { - model: db.QuestionnaireQuestion, + model: QuestionnaireQuestion, as: 'question', - include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }] + include: [{ model: QuestionnaireOption, as: 'questionOptions' }] } ] }, { - model: db.RequestParticipant, + model: RequestParticipant, as: 'participants', separate: true, - include: [{ model: db.User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }] + include: [{ model: User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }] }, { - model: db.OnboardingDocument, + model: OnboardingDocument, as: 'uploadedDocuments', separate: true, - include: [{ model: db.User, as: 'uploader', attributes: ['fullName', 'roleCode'] }], + include: [{ model: User, as: 'uploader', attributes: ['fullName', 'roleCode'] }], order: [['createdAt', 'DESC']] }, - { model: db.StageApprovalAction, as: 'stageApprovals', separate: true }, - { model: db.DealerCode, as: 'dealerCode' }, - { model: db.Dealer, as: 'dealer' } + { model: StageApprovalAction, as: 'stageApprovals', separate: true }, + { model: DealerCode, as: 'dealerCode' }, + { model: Dealer, as: 'dealer' }, + { + model: FddAssignment, + as: 'fddAssignments', + include: [ + { + model: FddReport, + as: 'reports', + include: [{ model: OnboardingDocument, as: 'reportDocument' }] + } + ] + }, + { + model: Worknote, + as: 'worknotes', + separate: true, + include: [{ model: User, as: 'author', attributes: ['id', 'fullName', 'email', 'roleCode'] }], + order: [['createdAt', 'DESC']] + } ] }); @@ -252,9 +274,20 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => { return res.json({ success: true, data: restrictedData }); } - // Security Check: Ensure prospective dealer controls data ownership - if (req.user?.roleCode === 'Prospective Dealer' && application.email !== req.user.email) { - return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' }); + // Security Check: Ensure prospective dealer controls data ownership and document privacy + if (req.user?.roleCode === 'Prospective Dealer') { + if (application.email !== req.user.email) { + return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' }); + } + + // FILTER: Prospect should ONLY see documents they uploaded + const restrictedData = application.toJSON(); + if (restrictedData.uploadedDocuments) { + restrictedData.uploadedDocuments = (restrictedData.uploadedDocuments as any[]).filter( + (doc: any) => doc.uploadedBy === req.user?.id || (doc.uploadedBy === null && doc.applicationId === application.id) + ); + } + return res.json({ success: true, data: restrictedData }); } res.json({ success: true, data: application }); @@ -403,11 +436,21 @@ export const getApplicationDocuments = async (req: AuthRequest, res: Response) = return res.status(404).json({ success: false, message: 'Application not found' }); } + const whereClause: any = { + applicationId: application.id, + status: 'active' + }; + + // ENFORCE PRIVACY: Prospect should ONLY see documents they uploaded + if (req.user?.roleCode === 'Prospective Dealer') { + whereClause[Op.or] = [ + { uploadedBy: req.user?.id || null }, + { uploadedBy: null } + ]; + } + const documents = await db.OnboardingDocument.findAll({ - where: { - applicationId: application.id, - status: 'active' - }, + where: whereClause, include: [ { model: db.User, as: 'uploader', attributes: ['fullName'] } ], diff --git a/src/modules/settlement/settlement.controller.ts b/src/modules/settlement/settlement.controller.ts index 3674357..83d3cc5 100644 --- a/src/modules/settlement/settlement.controller.ts +++ b/src/modules/settlement/settlement.controller.ts @@ -23,19 +23,35 @@ export const getOnboardingPayments = async (req: Request, res: Response) => { export const updatePayment = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { paidDate, amount, transactionReference, status } = req.body; + const { paidDate, amount, transactionReference, status, remarks } = req.body; + const payment = await FinancePayment.findByPk(id); if (!payment) return res.status(404).json({ success: false, message: 'Payment not found' }); + const isVerifying = status === 'Paid' && payment.paymentStatus !== 'Paid'; + await payment.update({ paymentDate: paidDate || payment.paymentDate, amount: amount || payment.amount, transactionId: transactionReference || payment.transactionId, paymentStatus: status || payment.paymentStatus, + remarks: remarks || payment.remarks, + verifiedBy: isVerifying ? req.user?.id : payment.verifiedBy, + verificationDate: isVerifying ? new Date() : payment.verificationDate, updatedAt: new Date() }); - res.json({ success: true, message: 'Payment updated successfully' }); + + // Re-fetch with verifier details for frontend + const updatedPayment = await FinancePayment.findByPk(id, { + include: [ + { model: Application, as: 'application', attributes: ['applicantName', 'applicationId'] }, + { model: User, as: 'verifier', attributes: ['fullName'] } + ] + }); + + res.json({ success: true, message: 'Payment updated successfully', data: updatedPayment }); } catch (error) { + console.error('Update payment error:', error); res.status(500).json({ success: false, message: 'Error updating payment' }); } }; diff --git a/src/services/WorkflowService.ts b/src/services/WorkflowService.ts index 771cc1b..41af470 100644 --- a/src/services/WorkflowService.ts +++ b/src/services/WorkflowService.ts @@ -10,7 +10,13 @@ export class WorkflowService { */ static async transitionApplication(application: any, targetStatus: string, userId: string | null = null, metadata: any = {}) { const previousStatus = application.overallStatus; - const { reason, stage, progressPercentage } = metadata; + const { reason, stage, progressPercentage, forceLog } = metadata; + + // Skip redundant history logging if status is identical (unless forced) + if (targetStatus === previousStatus && !forceLog) { + console.log(`[WorkflowService] Status already at ${targetStatus}. Skipping redundant log.`); + return application; + } const updateData: any = { overallStatus: targetStatus, @@ -39,13 +45,27 @@ export class WorkflowService { changeReason: reason || `Transitioned to ${targetStatus}` }); - // 3. Create Audit Log + // 3. Create High-Fidelity Audit Log await AuditLog.create({ userId: userId, action: AUDIT_ACTIONS.UPDATED, entityType: 'application', entityId: application.id, - newData: { status: targetStatus, stage: stage || application.currentStage } + oldData: { + status: previousStatus, + stage: application.currentStage, + progress: application.progressPercentage + }, + newData: { + status: targetStatus, + stage: stage || application.currentStage, + progress: progressPercentage ?? application.progressPercentage, + reason: reason || `Transitioned to ${targetStatus}` + }, + metadata: { + ...metadata, + timestamp: new Date() + } }); // 4. Synchronize Progress Tracker (The true source of truth for the frontend UI)