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',
|
LEGAL: 'Legal',
|
||||||
ARCHITECTURE: 'Architecture Team',
|
ARCHITECTURE: 'Architecture Team',
|
||||||
FINANCE: 'Finance',
|
FINANCE: 'Finance',
|
||||||
|
FDD: 'FDD',
|
||||||
|
LOI: 'LOI',
|
||||||
|
LOA: 'LOA',
|
||||||
|
EOR: 'EOR',
|
||||||
LEVEL_1_APPROVED: 'Level 1 Approved',
|
LEVEL_1_APPROVED: 'Level 1 Approved',
|
||||||
LEVEL_2_APPROVED: 'Level 2 Approved',
|
LEVEL_2_APPROVED: 'Level 2 Approved',
|
||||||
LEVEL_2_RECOMMENDED: 'Level 2 Recommended',
|
LEVEL_2_RECOMMENDED: 'Level 2 Recommended',
|
||||||
@ -96,7 +100,8 @@ export const APPLICATION_STATUS = {
|
|||||||
INAUGURATION: 'Inauguration',
|
INAUGURATION: 'Inauguration',
|
||||||
ONBOARDED: 'Onboarded',
|
ONBOARDED: 'Onboarded',
|
||||||
DISQUALIFIED: 'Disqualified',
|
DISQUALIFIED: 'Disqualified',
|
||||||
LOI_REJECTED: 'LOI Rejected'
|
LOI_REJECTED: 'LOI Rejected',
|
||||||
|
RETURNED_TO_FDD: 'Returned to FDD'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Termination Stages
|
// Termination Stages
|
||||||
@ -389,6 +394,8 @@ export const DOCUMENT_TYPES = {
|
|||||||
RELOCATION_BUILDING_PLAN: 'Building plan approval',
|
RELOCATION_BUILDING_PLAN: 'Building plan approval',
|
||||||
RELOCATION_ELECTRICITY_DOCS: 'Electricity connection documents',
|
RELOCATION_ELECTRICITY_DOCS: 'Electricity connection documents',
|
||||||
RELOCATION_WATER_DOCS: 'Water supply documents',
|
RELOCATION_WATER_DOCS: 'Water supply documents',
|
||||||
|
INCOME_TAX_RETURNS: 'Income Tax Returns (ITR)',
|
||||||
|
BUSINESS_VALUATION_REPORT: 'Business Valuation Report',
|
||||||
OTHER: 'Other'
|
OTHER: 'Other'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@ -120,6 +120,14 @@ export const syncApplicationProgress = async (applicationId: string, overallStat
|
|||||||
|
|
||||||
|
|
||||||
await updateApplicationProgress(applicationId, currentStageName, isCompleted ? 'completed' : 'active', isCompleted ? 100 : 50);
|
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.hasOne(models.Dealer, { foreignKey: 'applicationId', as: 'dealer' });
|
||||||
Application.hasMany(models.SecurityDeposit, { foreignKey: 'applicationId', as: 'securityDeposits' });
|
Application.hasMany(models.SecurityDeposit, { foreignKey: 'applicationId', as: 'securityDeposits' });
|
||||||
Application.hasOne(models.EorChecklist, { foreignKey: 'applicationId', as: 'eorChecklist' });
|
Application.hasOne(models.EorChecklist, { foreignKey: 'applicationId', as: 'eorChecklist' });
|
||||||
|
Application.hasMany(models.FddAssignment, { foreignKey: 'applicationId', as: 'fddAssignments' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return Application;
|
return Application;
|
||||||
|
|||||||
@ -53,9 +53,10 @@ const processStageDecision = async (params: {
|
|||||||
roleCode: string;
|
roleCode: string;
|
||||||
interviewId?: string;
|
interviewId?: string;
|
||||||
nextStatus?: string;
|
nextStatus?: string;
|
||||||
|
nextStage?: string;
|
||||||
nextProgress?: number;
|
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 } });
|
const policy = await db.StageApprovalPolicy.findOne({ where: { stageCode } });
|
||||||
if (!policy) return { noPolicy: true };
|
if (!policy) return { noPolicy: true };
|
||||||
@ -132,14 +133,32 @@ const processStageDecision = async (params: {
|
|||||||
});
|
});
|
||||||
statusUpdated = true;
|
statusUpdated = true;
|
||||||
}
|
}
|
||||||
} else if (evaluation.policyMet && nextStatus) {
|
} else if (evaluation.policyMet) {
|
||||||
const application = await db.Application.findByPk(applicationId);
|
const application = await db.Application.findByPk(applicationId);
|
||||||
if (application) {
|
if (application) {
|
||||||
await WorkflowService.transitionApplication(application, nextStatus, userId, {
|
let targetStatus = nextStatus;
|
||||||
reason: `Policy met for ${stageCode}`,
|
let targetStage = nextStage;
|
||||||
progressPercentage: nextProgress
|
let targetProgress = nextProgress;
|
||||||
});
|
|
||||||
statusUpdated = true;
|
// 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
|
// Ensure policy exists for interviews
|
||||||
await ensureInterviewPolicy(interview.level);
|
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 progressMap: any = { 1: 40, 2: 55, 3: 70 };
|
||||||
|
|
||||||
const result = await processStageDecision({
|
const result = await processStageDecision({
|
||||||
applicationId: interview.applicationId,
|
applicationId: interview.applicationId,
|
||||||
stageCode,
|
stageCode,
|
||||||
@ -183,6 +203,7 @@ const processInterviewApprovalDecision = async (params: {
|
|||||||
roleCode,
|
roleCode,
|
||||||
interviewId,
|
interviewId,
|
||||||
nextStatus: nextStatusMap[interview.level] || 'Approved',
|
nextStatus: nextStatusMap[interview.level] || 'Approved',
|
||||||
|
nextStage: nextStageMap[interview.level] || APPLICATION_STAGES.APPROVED,
|
||||||
nextProgress: progressMap[interview.level]
|
nextProgress: progressMap[interview.level]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
|||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { FddAssignment, FddReport, AuditLog, Application } = db;
|
const { FddAssignment, FddReport, AuditLog, Application } = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
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';
|
import { WorkflowService } from '../../services/WorkflowService.js';
|
||||||
|
|
||||||
export const getAssignment = async (req: Request, res: Response) => {
|
export const getAssignment = async (req: Request, res: Response) => {
|
||||||
@ -29,9 +29,19 @@ export const assignAgency = async (req: AuthRequest, res: Response) => {
|
|||||||
status: 'Assigned'
|
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({
|
await AuditLog.create({
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
action: AUDIT_ACTIONS.CREATED,
|
action: AUDIT_ACTIONS.FDD_ASSIGNED,
|
||||||
entityType: 'fdd_assignment',
|
entityType: 'fdd_assignment',
|
||||||
entityId: assignment.id
|
entityId: assignment.id
|
||||||
});
|
});
|
||||||
@ -67,10 +77,61 @@ export const uploadReport = async (req: AuthRequest, res: Response) => {
|
|||||||
if (assignmentRecord) {
|
if (assignmentRecord) {
|
||||||
const application = await Application.findByPk(assignmentRecord.applicationId);
|
const application = await Application.findByPk(assignmentRecord.applicationId);
|
||||||
if (application) {
|
if (application) {
|
||||||
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.SECURITY_DETAILS, req.user?.id || null, {
|
// Ensure LOI Request exists for the next stage
|
||||||
reason: 'FDD Report submitted by agency',
|
const [loiRequest] = await db.LoiRequest.findOrCreate({
|
||||||
progressPercentage: 70
|
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';
|
import db from '../../database/models/index.js';
|
||||||
const { LoaRequest, LoaApproval, LoaDocumentGenerated, SecurityDeposit, AuditLog, StageApprovalPolicy, StageApprovalAction, User } = db;
|
const { LoaRequest, LoaApproval, LoaDocumentGenerated, SecurityDeposit, AuditLog, StageApprovalPolicy, StageApprovalAction, User } = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
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';
|
import { WorkflowService } from '../../services/WorkflowService.js';
|
||||||
|
|
||||||
const LOA_STAGE_CODE = 'LOA_APPROVAL';
|
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' });
|
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') {
|
if (action === 'Approved') {
|
||||||
const finalDeposit = await SecurityDeposit.findOne({
|
const finalDeposit = await SecurityDeposit.findOne({
|
||||||
where: { applicationId: request.applicationId, depositType: 'FINAL', status: 'Verified' }
|
where: {
|
||||||
|
applicationId: request.applicationId,
|
||||||
|
depositType: 'FINAL',
|
||||||
|
status: 'Verified'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!finalDeposit) {
|
if (!finalDeposit) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
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) {
|
if (deposit) {
|
||||||
await deposit.update({
|
await deposit.update({
|
||||||
amount, paymentReference, proofDocumentId, status,
|
amount, paymentReference, proofDocumentId, status,
|
||||||
verifiedBy: status === 'Verified' ? req.user?.id : null,
|
verifiedBy: status === 'Verified' ? req.user?.id : deposit.verifiedBy,
|
||||||
verifiedAt: status === 'Verified' ? new Date() : null
|
verifiedAt: status === 'Verified' && !deposit.verifiedAt ? new Date() : deposit.verifiedAt
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
deposit = await SecurityDeposit.create({
|
deposit = await SecurityDeposit.create({
|
||||||
@ -268,73 +273,84 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) =>
|
|||||||
paymentReference,
|
paymentReference,
|
||||||
proofDocumentId,
|
proofDocumentId,
|
||||||
status: status || 'Pending',
|
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
|
// --- FETCH WITH JOIN FOR FRONTEND ---
|
||||||
if (depositType === 'INITIAL' && status === 'Verified') {
|
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 LoiRequest = db.LoiRequest;
|
||||||
const LoiApproval = db.LoiApproval;
|
const LoiApproval = db.LoiApproval;
|
||||||
const StageApprovalAction = db.StageApprovalAction;
|
|
||||||
|
|
||||||
const loiReq = await LoiRequest.findOne({ where: { applicationId: application.id } });
|
// 1. Proactively ensure the LOI Request exists if the payment is cleared
|
||||||
if (loiReq) {
|
const [loiReq, createdReq] = await db.LoiRequest.findOrCreate({
|
||||||
// 1. Update module-specific approval table
|
where: { applicationId: application.id },
|
||||||
const financeApproval = await LoiApproval.findOne({
|
defaults: { status: 'Pending Approval' }
|
||||||
where: { requestId: loiReq.id, approverRole: 'Finance', action: 'Pending' }
|
});
|
||||||
});
|
console.log(`[DEBUG] LOI Request ID: ${loiReq.id}, Status: ${loiReq.status} (Created: ${createdReq})`);
|
||||||
if (financeApproval) {
|
|
||||||
await financeApproval.update({
|
|
||||||
action: 'Approved',
|
|
||||||
remarks: 'Auto-approved via Finance payment verification',
|
|
||||||
approverId: req.user?.id,
|
|
||||||
approvedAt: new Date()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Update generic StageApprovalAction table
|
// 2. Initialize the three required approval roles for the LOI step
|
||||||
await StageApprovalAction.upsert({
|
const roles = ['Finance', 'DD Head', 'NBH'];
|
||||||
applicationId: application.id,
|
await Promise.all(roles.map(async (role) => {
|
||||||
stageCode: 'LOI_APPROVAL',
|
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,
|
actorUserId: req.user?.id,
|
||||||
actorRole: 'Finance',
|
actionedAt: new Date(),
|
||||||
decision: 'Approved',
|
comments: 'Initial Security Deposit verified.'
|
||||||
remarks: 'Auto-approved via Finance payment verification'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Check if LOI can now be fully approved (copied logic from loi.controller)
|
console.log(`[DEBUG] Initial Security Deposit verified. Transitioning to LOI Issued...`);
|
||||||
const policy = await db.StageApprovalPolicy.findOne({ where: { stageCode: 'LOI_APPROVAL' } });
|
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_ISSUED, req.user?.id || null, {
|
||||||
const requiredRoles = policy?.requiredRoles || ['Finance', 'DD Head', 'NBH'];
|
reason: 'Initial Security Deposit verified. Proceeding to LOI Issuance.',
|
||||||
|
stage: APPLICATION_STAGES.LOI,
|
||||||
const stageActions = await StageApprovalAction.findAll({
|
progressPercentage: 80
|
||||||
where: { applicationId: application.id, stageCode: 'LOI_APPROVAL' }
|
|
||||||
});
|
});
|
||||||
const approvedRoles = new Set(stageActions.filter((a: any) => a.decision === 'Approved').map((a: any) => a.actorRole));
|
} else {
|
||||||
const meetsMinApprovals = approvedRoles.size >= (policy?.minApprovals || 3);
|
console.log(`[DEBUG] No pending Finance approval in LOI stage. Skipping auto-bridge.`);
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Update Security Deposit error:', error);
|
console.error('Update Security Deposit error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error updating security deposit' });
|
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({
|
const deposits = await SecurityDeposit.findAll({
|
||||||
where: { applicationId: application.id },
|
where: { applicationId: application.id },
|
||||||
|
include: [{ model: User, as: 'verifier', attributes: ['fullName'] }],
|
||||||
order: [['createdAt', 'ASC']]
|
order: [['createdAt', 'ASC']]
|
||||||
});
|
});
|
||||||
res.json({ success: true, data: deposits });
|
res.json({ success: true, data: deposits });
|
||||||
|
|||||||
@ -294,12 +294,17 @@ export const generateDocument = async (req: AuthRequest, res: Response) => {
|
|||||||
filePath: `/uploads/loi/${mockFile}`
|
filePath: `/uploads/loi/${mockFile}`
|
||||||
});
|
});
|
||||||
|
|
||||||
await AuditLog.create({
|
// Bridge: Transition from LOI Issued -> Dealer Code Generation
|
||||||
userId: req.user?.id,
|
const request = await LoiRequest.findByPk(requestId);
|
||||||
action: AUDIT_ACTIONS.LOI_GENERATED,
|
if (request) {
|
||||||
entityType: 'loi_request',
|
const application = await db.Application.findByPk(request.applicationId);
|
||||||
entityId: requestId
|
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 });
|
res.json({ success: true, message: 'LOI Document generated (Mock)', data: doc });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
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 { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
@ -181,35 +181,57 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
|
|||||||
include: [
|
include: [
|
||||||
{ model: ApplicationStatusHistory, as: 'statusHistory', separate: true, order: [['createdAt', 'DESC']] },
|
{ model: ApplicationStatusHistory, as: 'statusHistory', separate: true, order: [['createdAt', 'DESC']] },
|
||||||
{ model: ApplicationProgress, as: 'progressTracking', separate: true, order: [['stageOrder', 'ASC']] },
|
{ 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',
|
as: 'questionnaireResponses',
|
||||||
separate: true,
|
separate: true,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: db.QuestionnaireQuestion,
|
model: QuestionnaireQuestion,
|
||||||
as: 'question',
|
as: 'question',
|
||||||
include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }]
|
include: [{ model: QuestionnaireOption, as: 'questionOptions' }]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: db.RequestParticipant,
|
model: RequestParticipant,
|
||||||
as: 'participants',
|
as: 'participants',
|
||||||
separate: true,
|
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',
|
as: 'uploadedDocuments',
|
||||||
separate: true,
|
separate: true,
|
||||||
include: [{ model: db.User, as: 'uploader', attributes: ['fullName', 'roleCode'] }],
|
include: [{ model: User, as: 'uploader', attributes: ['fullName', 'roleCode'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
},
|
},
|
||||||
{ model: db.StageApprovalAction, as: 'stageApprovals', separate: true },
|
{ model: StageApprovalAction, as: 'stageApprovals', separate: true },
|
||||||
{ model: db.DealerCode, as: 'dealerCode' },
|
{ model: DealerCode, as: 'dealerCode' },
|
||||||
{ model: db.Dealer, as: 'dealer' }
|
{ 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 });
|
return res.json({ success: true, data: restrictedData });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security Check: Ensure prospective dealer controls data ownership
|
// Security Check: Ensure prospective dealer controls data ownership and document privacy
|
||||||
if (req.user?.roleCode === 'Prospective Dealer' && application.email !== req.user.email) {
|
if (req.user?.roleCode === 'Prospective Dealer') {
|
||||||
return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' });
|
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 });
|
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' });
|
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({
|
const documents = await db.OnboardingDocument.findAll({
|
||||||
where: {
|
where: whereClause,
|
||||||
applicationId: application.id,
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
include: [
|
include: [
|
||||||
{ model: db.User, as: 'uploader', attributes: ['fullName'] }
|
{ 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) => {
|
export const updatePayment = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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);
|
const payment = await FinancePayment.findByPk(id);
|
||||||
if (!payment) return res.status(404).json({ success: false, message: 'Payment not found' });
|
if (!payment) return res.status(404).json({ success: false, message: 'Payment not found' });
|
||||||
|
|
||||||
|
const isVerifying = status === 'Paid' && payment.paymentStatus !== 'Paid';
|
||||||
|
|
||||||
await payment.update({
|
await payment.update({
|
||||||
paymentDate: paidDate || payment.paymentDate,
|
paymentDate: paidDate || payment.paymentDate,
|
||||||
amount: amount || payment.amount,
|
amount: amount || payment.amount,
|
||||||
transactionId: transactionReference || payment.transactionId,
|
transactionId: transactionReference || payment.transactionId,
|
||||||
paymentStatus: status || payment.paymentStatus,
|
paymentStatus: status || payment.paymentStatus,
|
||||||
|
remarks: remarks || payment.remarks,
|
||||||
|
verifiedBy: isVerifying ? req.user?.id : payment.verifiedBy,
|
||||||
|
verificationDate: isVerifying ? new Date() : payment.verificationDate,
|
||||||
updatedAt: new Date()
|
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) {
|
} catch (error) {
|
||||||
|
console.error('Update payment error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error updating payment' });
|
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 = {}) {
|
static async transitionApplication(application: any, targetStatus: string, userId: string | null = null, metadata: any = {}) {
|
||||||
const previousStatus = application.overallStatus;
|
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 = {
|
const updateData: any = {
|
||||||
overallStatus: targetStatus,
|
overallStatus: targetStatus,
|
||||||
@ -39,13 +45,27 @@ export class WorkflowService {
|
|||||||
changeReason: reason || `Transitioned to ${targetStatus}`
|
changeReason: reason || `Transitioned to ${targetStatus}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Create Audit Log
|
// 3. Create High-Fidelity Audit Log
|
||||||
await AuditLog.create({
|
await AuditLog.create({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
action: AUDIT_ACTIONS.UPDATED,
|
action: AUDIT_ACTIONS.UPDATED,
|
||||||
entityType: 'application',
|
entityType: 'application',
|
||||||
entityId: application.id,
|
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)
|
// 4. Synchronize Progress Tracker (The true source of truth for the frontend UI)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user