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

This commit is contained in:
laxman h 2026-04-06 19:11:46 +05:30
parent 25d5570319
commit 8dbe83e230
18 changed files with 517 additions and 104 deletions

18
check_bug_app.ts Normal file
View File

@ -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();

20
check_db.ts Normal file
View File

@ -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();

16
check_final.ts Normal file
View File

@ -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();

25
check_loi.ts Normal file
View File

@ -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();

21
check_new_app.ts Normal file
View File

@ -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();

15
repair_fdd_stage.ts Normal file
View File

@ -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();

35
repair_loi.ts Normal file
View File

@ -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();

64
reset_apps.ts Normal file
View File

@ -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();

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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]
});

View File

@ -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 || [])}`);
}
}

View File

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

View File

@ -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) {

View File

@ -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'] }
],

View File

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

View File

@ -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)