From 8dbe83e230291b80028d8eee990503e3d203dab0 Mon Sep 17 00:00:00 2001 From: laxman h Date: Mon, 6 Apr 2026 19:11:46 +0530 Subject: [PATCH] delaer onboarding end to end fleo checked and also made some new changes in the progress trck now after the LOI approvl finance and securaty details kept separate for approval also LOI issue is differnt approval --- check_bug_app.ts | 18 +++ check_db.ts | 20 +++ check_final.ts | 16 +++ check_loi.ts | 25 ++++ check_new_app.ts | 21 +++ repair_fdd_stage.ts | 15 ++ repair_loi.ts | 35 +++++ reset_apps.ts | 64 +++++++++ src/common/config/constants.ts | 9 +- src/common/utils/progress.ts | 8 ++ src/database/models/Application.ts | 1 + .../assessment/assessment.controller.ts | 39 +++-- src/modules/fdd/fdd.controller.ts | 71 ++++++++- src/modules/loa/loa.controller.ts | 135 ++++++++++-------- src/modules/loi/loi.controller.ts | 17 ++- .../onboarding/onboarding.controller.ts | 81 ++++++++--- .../settlement/settlement.controller.ts | 20 ++- src/services/WorkflowService.ts | 26 +++- 18 files changed, 517 insertions(+), 104 deletions(-) create mode 100644 check_bug_app.ts create mode 100644 check_db.ts create mode 100644 check_final.ts create mode 100644 check_loi.ts create mode 100644 check_new_app.ts create mode 100644 repair_fdd_stage.ts create mode 100644 repair_loi.ts create mode 100644 reset_apps.ts 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)