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:
parent
25d5570319
commit
8dbe83e230
18
check_bug_app.ts
Normal file
18
check_bug_app.ts
Normal 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
20
check_db.ts
Normal 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
16
check_final.ts
Normal 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
25
check_loi.ts
Normal 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
21
check_new_app.ts
Normal 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
15
repair_fdd_stage.ts
Normal 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
35
repair_loi.ts
Normal 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
64
reset_apps.ts
Normal 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();
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]
|
||||
});
|
||||
|
||||
|
||||
@ -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 || [])}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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'] }
|
||||
],
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user