dealer onboarding dashboard flow changes made

This commit is contained in:
laxman h 2026-04-08 19:01:25 +05:30
parent 37ecf3ba85
commit 65d2af7447
15 changed files with 408 additions and 211 deletions

8
check_policies.ts Normal file
View File

@ -0,0 +1,8 @@
import db from './src/database/models/index.js';
async function checkPolicies() {
const policies = await db.StageApprovalPolicy.findAll();
console.log(JSON.stringify(policies, null, 2));
}
checkPolicies().catch(console.error);

18
repair_all_progress.ts Normal file
View File

@ -0,0 +1,18 @@
import db from './src/database/models/index.js';
import { syncApplicationProgress } from './src/common/utils/progress.js';
async function repairAllProgress() {
console.log('--- Repairing Progress Tracker State for ALL Applications ---');
const apps = await db.Application.findAll();
console.log(`Found ${apps.length} applications to sync.`);
for (const app of apps) {
console.log(`Syncing Progress for Application: ${app.applicationId} (Status: ${app.overallStatus})`);
await syncApplicationProgress(app.id, app.overallStatus);
}
console.log('--- REPAIR COMPLETE ---');
}
repairAllProgress().catch(console.error).then(() => process.exit(0));

27
repair_fdd_policy.ts Normal file
View File

@ -0,0 +1,27 @@
import db from './src/database/models/index.js';
async function repairPolicy() {
console.log('--- Repairing FDD_VERIFICATION Policy ---');
// Update FDD_VERIFICATION to allow Finance and DD Admin
const [policy] = await db.StageApprovalPolicy.findOrCreate({
where: { stageCode: 'FDD_VERIFICATION' },
defaults: {
stageCode: 'FDD_VERIFICATION',
minApprovals: 1,
approvalMode: 'ROLE_MANDATORY',
requiredRoles: ['FDD', 'Finance', 'DD Admin', 'Finance Admin'],
isActive: true
}
});
if (policy) {
await policy.update({
requiredRoles: ['FDD', 'Finance', 'DD Admin', 'Finance Admin', 'DD Head'],
minApprovals: 1
});
console.log('Policy updated successfully:', policy.requiredRoles);
}
}
repairPolicy().catch(console.error).then(() => process.exit(0));

View File

@ -27,9 +27,9 @@ const policies = [
}, },
{ {
stageCode: 'LOI_APPROVAL', stageCode: 'LOI_APPROVAL',
minApprovals: 3, minApprovals: 2,
approvalMode: 'ROLE_MANDATORY', approvalMode: 'ROLE_MANDATORY',
requiredRoles: ['Finance', 'DD Head', 'NBH'], requiredRoles: ['DD Head', 'NBH'],
isActive: true isActive: true
}, },
{ {
@ -43,7 +43,7 @@ const policies = [
stageCode: 'FDD_VERIFICATION', stageCode: 'FDD_VERIFICATION',
minApprovals: 1, minApprovals: 1,
approvalMode: 'ROLE_MANDATORY', approvalMode: 'ROLE_MANDATORY',
requiredRoles: ['FDD'], requiredRoles: ['DD Admin', 'Super Admin'],
isActive: true isActive: true
} }
]; ];

View File

@ -41,8 +41,8 @@ const configs = [
{ documentType: 'Statutory Approval Certificate', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] }, { documentType: 'Statutory Approval Certificate', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
// LOI / Security (Approval Process) // LOI / Security (Approval Process)
{ documentType: 'Initial Security Deposit Receipt', stageCode: 'LOI Approval', allowedRoles: [ROLES.DEALER, ROLES.FINANCE, ROLES.DD_HEAD, ROLES.NBH, ROLES.SUPER_ADMIN], isMandatory: true }, { documentType: 'Security Deposit Receipt', stageCode: 'LOI Approval', allowedRoles: [ROLES.DEALER, ROLES.FINANCE, ROLES.DD_HEAD, ROLES.NBH, ROLES.SUPER_ADMIN], isMandatory: true },
{ documentType: 'Final Security Deposit Receipt', stageCode: 'LOA Approval', allowedRoles: [ROLES.DEALER, ROLES.FINANCE, ROLES.DD_HEAD, ROLES.NBH, ROLES.SUPER_ADMIN], isMandatory: true }, { documentType: 'First Fill Receipt', stageCode: 'LOA Approval', allowedRoles: [ROLES.DEALER, ROLES.FINANCE, ROLES.DD_HEAD, ROLES.NBH, ROLES.SUPER_ADMIN], isMandatory: true },
{ documentType: 'LOI Acknowledgement Copy', stageCode: 'LOI Issue', allowedRoles: ALL_ROLES }, { documentType: 'LOI Acknowledgement Copy', stageCode: 'LOI Issue', allowedRoles: ALL_ROLES },
{ documentType: 'Nodal Agreement', stageCode: 'LOI Approval', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.DEALER, ROLES.SUPER_ADMIN] }, { documentType: 'Nodal Agreement', stageCode: 'LOI Approval', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.DEALER, ROLES.SUPER_ADMIN] },

View File

@ -12,16 +12,16 @@ const seedSystemConfigs = async () => {
const configs = [ const configs = [
{ {
key: 'INITIAL_SECURITY_DEPOSIT', key: 'SECURITY_DEPOSIT',
value: { amount: 500000, currency: 'INR' }, value: { amount: 500000, currency: 'INR' },
category: 'SECURITY_DEPOSIT', category: 'SECURITY_DEPOSIT',
description: 'Default Initial Security Deposit amount for new dealer onboarding' description: 'Default Security Deposit amount for new dealer onboarding'
}, },
{ {
key: 'FINAL_SECURITY_DEPOSIT', key: 'FIRST_FILL',
value: { amount: 1500000, currency: 'INR' }, value: { amount: 1500000, currency: 'INR' },
category: 'SECURITY_DEPOSIT', category: 'SECURITY_DEPOSIT',
description: 'Default Final Security Deposit amount for new dealer onboarding' description: 'Default First Fill amount for new dealer onboarding'
} }
]; ];

View File

@ -380,8 +380,8 @@ export const DOCUMENT_TYPES = {
STATUTORY_AUDIT: 'Statutory Approval Certificate', STATUTORY_AUDIT: 'Statutory Approval Certificate',
BANK_GUARANTEE: 'Bank Guarantee Document', BANK_GUARANTEE: 'Bank Guarantee Document',
SECURITY_DEPOSIT_RECEIPT: 'Security Deposit Receipt', SECURITY_DEPOSIT_RECEIPT: 'Security Deposit Receipt',
SECURITY_DEPOSIT_INITIAL: 'Initial Security Deposit Receipt', SECURITY_DEPOSIT: 'Security Deposit Receipt',
SECURITY_DEPOSIT_FINAL: 'Final Security Deposit Receipt', FIRST_FILL: 'First Fill Receipt',
RELOCATION_PROPERTY_DOCS: 'Property documents for new location', RELOCATION_PROPERTY_DOCS: 'Property documents for new location',
RELOCATION_LEASE_AGREEMENT: 'Lease/Rental agreement for new location', RELOCATION_LEASE_AGREEMENT: 'Lease/Rental agreement for new location',
RELOCATION_NOC_LANDLORD: 'NOC from current landlord', RELOCATION_NOC_LANDLORD: 'NOC from current landlord',

View File

@ -50,8 +50,22 @@ export const updateApplicationProgress = async (applicationId: string, stageName
} }
// Whenever a stage is marked 'active' or 'completed', // Whenever a stage is marked 'active' or 'completed',
// all previous stages MUST be completed. // all previous stages MUST exist and be marked 'completed'.
if (status === 'active' || status === 'completed') { if (status === 'active' || status === 'completed') {
const previousStages = ONBOARDING_STAGES.filter(s => s.order < stage.order);
for (const prev of previousStages) {
await ApplicationProgress.findOrCreate({
where: { applicationId, stageName: prev.name },
defaults: {
stageOrder: prev.order,
status: 'completed',
completionPercentage: 100,
stageCompletedAt: new Date()
}
});
}
// Also update any existing ones that weren't completed
await ApplicationProgress.update( await ApplicationProgress.update(
{ status: 'completed', completionPercentage: 100, stageCompletedAt: new Date() }, { status: 'completed', completionPercentage: 100, stageCompletedAt: new Date() },
{ {
@ -114,8 +128,9 @@ export const syncApplicationProgress = async (applicationId: string, overallStat
// Determine status for this stage // Determine status for this stage
const isCompleted = [ const isCompleted = [
'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved', 'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved',
'Level 2 Approved', 'Level 3 Approved', 'LOI Issued', 'EOR Complete', 'Level 2 Approved', 'Level 3 Approved', 'FDD Verification', 'LOI Issued',
'Approved', 'Onboarded' 'Dealer Code Generation', 'Architecture Team Completion', 'LOA Issued',
'EOR Complete', 'Approved', 'Onboarded'
].includes(overallStatus); ].includes(overallStatus);

View File

@ -58,6 +58,16 @@ const processStageDecision = async (params: {
}) => { }) => {
const { applicationId, stageCode, decision, remarks, userId, roleCode, interviewId, nextStatus, nextStage, nextProgress } = params; const { applicationId, stageCode, decision, remarks, userId, roleCode, interviewId, nextStatus, nextStage, nextProgress } = params;
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await db.Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) return { notFound: true };
const resolvedId = application.id;
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 };
@ -65,7 +75,7 @@ const processStageDecision = async (params: {
// Check if user is an assigned participant // Check if user is an assigned participant
const userAssignments = await db.RequestParticipant.findAll({ const userAssignments = await db.RequestParticipant.findAll({
where: { requestId: applicationId, requestType: 'application', userId } where: { requestId: resolvedId, requestType: 'application', userId }
}); });
const isAssigned = userAssignments.some((p: any) => { const isAssigned = userAssignments.some((p: any) => {
@ -85,7 +95,7 @@ const processStageDecision = async (params: {
// --- Sequential Enforcement (SRS 6.16.2 & 6.18.3.1 Compliance) --- // --- Sequential Enforcement (SRS 6.16.2 & 6.18.3.1 Compliance) ---
if (roleCode !== 'Super Admin' && roleCode !== 'DD Admin') { if (roleCode !== 'Super Admin' && roleCode !== 'DD Admin') {
const approvedActions = await db.StageApprovalAction.findAll({ const approvedActions = await db.StageApprovalAction.findAll({
where: { applicationId, stageCode, decision: 'Approved' } where: { applicationId: resolvedId, stageCode, decision: 'Approved' }
}); });
const approvedRoles = new Set(approvedActions.map((a: any) => a.actorRole)); const approvedRoles = new Set(approvedActions.map((a: any) => a.actorRole));
@ -120,13 +130,13 @@ const processStageDecision = async (params: {
// Record Action - Robust handle for null interviewId which breaks unique constraint in Postgres // Record Action - Robust handle for null interviewId which breaks unique constraint in Postgres
if (!interviewId) { if (!interviewId) {
const existing = await db.StageApprovalAction.findOne({ const existing = await db.StageApprovalAction.findOne({
where: { applicationId, stageCode, actorUserId: userId, interviewId: null } where: { applicationId: resolvedId, stageCode, actorUserId: userId, interviewId: null }
}); });
if (existing) { if (existing) {
await existing.update({ decision, remarks: remarks || null, actorRole: assignedRole || roleCode }); await existing.update({ decision, remarks: remarks || null, actorRole: assignedRole || roleCode });
} else { } else {
await db.StageApprovalAction.create({ await db.StageApprovalAction.create({
applicationId, applicationId: resolvedId,
stageCode, stageCode,
actorUserId: userId, actorUserId: userId,
actorRole: assignedRole || roleCode, actorRole: assignedRole || roleCode,
@ -136,7 +146,7 @@ const processStageDecision = async (params: {
} }
} else { } else {
await db.StageApprovalAction.upsert({ await db.StageApprovalAction.upsert({
applicationId, applicationId: resolvedId,
interviewId: interviewId, interviewId: interviewId,
stageCode, stageCode,
actorUserId: userId, actorUserId: userId,
@ -153,14 +163,64 @@ const processStageDecision = async (params: {
); );
} }
// --- FDD Integration: Link approval to FddReport table for dashboard mapping ---
if (stageCode === 'FDD_VERIFICATION' && decision === 'Approved') {
const assignment = await db.FddAssignment.findOne({ where: { applicationId: resolvedId } });
if (assignment) {
// Find latest audit report document
const lastReportDoc = await db.OnboardingDocument.findOne({
where: { applicationId: resolvedId, documentType: 'FDD Final Audit Report' },
order: [['createdAt', 'DESC']]
});
// Parse structured recommendation/findings from remarks
let recommendation = 'Recommended';
let findings = remarks || 'Submission reviewed.';
if (remarks?.includes('[RECOMMENDATION:')) {
const parts = remarks.split('[RECOMMENDATION: ');
if (parts[1]) {
recommendation = parts[1].split(']')[0];
findings = remarks.split('\nFindings: ')[1] || remarks.split(']')[1]?.trim() || remarks;
}
}
await db.FddReport.create({
assignmentId: assignment.id,
reportDocumentId: lastReportDoc?.id || null,
findings,
recommendation,
verifiedAt: new Date(),
verifiedBy: userId
});
await assignment.update({ status: 'Report Submitted' });
// Bridge: Initialize LOI Records for the next stage (Moved from fdd.controller.ts for Admin Review flow)
console.log(`[DEBUG] FDD Approved by Admin. Initializing LOI Records for Application: ${resolvedId}`);
const [loiReq] = await db.LoiRequest.findOrCreate({
where: { applicationId: resolvedId },
defaults: { status: 'Pending Approval', requestedBy: userId }
});
const nextRoles = ['DD Head', 'NBH'];
await Promise.all(nextRoles.map(async (role) => {
await db.LoiApproval.findOrCreate({
where: { requestId: loiReq.id, approverRole: role },
defaults: { action: 'Pending', level: 1 }
});
}));
console.log(`[DEBUG] LOI Records initialized for ${nextRoles.join(', ')}`);
}
}
// Evaluate Policy via Centralized Service (FIXED unique user count) // Evaluate Policy via Centralized Service (FIXED unique user count)
const evaluation = await WorkflowService.evaluateStagePolicy(applicationId, stageCode); const evaluation = await WorkflowService.evaluateStagePolicy(resolvedId, stageCode);
const hasRejection = decision === 'Rejected'; // Immediate rejection if ANY required actor rejects (business rule) const hasRejection = decision === 'Rejected'; // Immediate rejection if ANY required actor rejects (business rule)
let statusUpdated = false; let statusUpdated = false;
if (hasRejection) { if (hasRejection) {
const application = await db.Application.findByPk(applicationId); const application = await db.Application.findByPk(resolvedId);
if (application) { if (application) {
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.REJECTED, userId, { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.REJECTED, userId, {
reason: `Rejected during ${stageCode} stage: ${remarks}`, reason: `Rejected during ${stageCode} stage: ${remarks}`,
@ -169,7 +229,7 @@ const processStageDecision = async (params: {
statusUpdated = true; statusUpdated = true;
} }
} else if (evaluation.policyMet) { } else if (evaluation.policyMet) {
const application = await db.Application.findByPk(applicationId); const application = await db.Application.findByPk(resolvedId);
if (application) { if (application) {
let targetStatus = nextStatus; let targetStatus = nextStatus;
let targetStage = nextStage; let targetStage = nextStage;
@ -181,7 +241,7 @@ const processStageDecision = async (params: {
targetStage = APPLICATION_STAGES.LOI; targetStage = APPLICATION_STAGES.LOI;
targetProgress = 75; targetProgress = 75;
} else if (stageCode === 'LOA_APPROVAL') { } else if (stageCode === 'LOA_APPROVAL') {
targetStatus = APPLICATION_STATUS.LOA_ISSUED; targetStatus = APPLICATION_STATUS.EOR_IN_PROGRESS;
targetStage = APPLICATION_STAGES.LOA; targetStage = APPLICATION_STAGES.LOA;
targetProgress = 95; targetProgress = 95;
} }
@ -225,9 +285,9 @@ 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: 'Level 3 Approved' }; const nextStatusMap: any = { 1: 'Level 1 Approved', 2: 'Level 2 Approved', 3: 'FDD Verification' };
const nextStageMap: any = { 1: APPLICATION_STAGES.LEVEL_1_APPROVED, 2: APPLICATION_STAGES.LEVEL_2_APPROVED, 3: APPLICATION_STAGES.FDD }; 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: 65 };
const result = await processStageDecision({ const result = await processStageDecision({
applicationId: interview.applicationId, applicationId: interview.applicationId,
@ -275,8 +335,9 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }] const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }]
// Find application UUID first (handles readable ID) // Find application UUID first (handles readable ID)
const application = await db.Application.findOne({ const _isUUID_qr = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(applicationId as string);
where: { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } const application = await db.Application.findOne({
where: _isUUID_qr ? { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } : { applicationId: applicationId }
}); });
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
@ -367,8 +428,9 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
const levelNum = typeof level === 'string' ? parseInt(level.replace(/\D/g, ''), 10) : level; const levelNum = typeof level === 'string' ? parseInt(level.replace(/\D/g, ''), 10) : level;
console.log(`Parsed Level: ${level} -> ${levelNum}`); console.log(`Parsed Level: ${level} -> ${levelNum}`);
const application = await db.Application.findOne({ const _isUUID_si = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(applicationId as string);
where: { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } const application = await db.Application.findOne({
where: _isUUID_si ? { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } : { applicationId: applicationId }
}); });
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
@ -1016,8 +1078,9 @@ export const submitStageDecision = async (req: AuthRequest, res: Response) => {
if (result.noPolicy) { if (result.noPolicy) {
// Fallback: If no policy, just update application status directly (legacy behavior) // Fallback: If no policy, just update application status directly (legacy behavior)
if (nextStatus) { if (nextStatus) {
const application = await db.Application.findOne({ const _isUUID_fb = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(applicationId as string);
where: { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } const application = await db.Application.findOne({
where: _isUUID_fb ? { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } : { applicationId: applicationId }
}); });
if (application) { if (application) {
await WorkflowService.transitionApplication(application, nextStatus, req.user?.id || null, { await WorkflowService.transitionApplication(application, nextStatus, req.user?.id || null, {

View File

@ -1,4 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Op } from 'sequelize';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { EorChecklist, EorChecklistItem, OnboardingDocument, RelocationDocument } = db; const { EorChecklist, EorChecklistItem, OnboardingDocument, RelocationDocument } = db;
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
@ -6,9 +7,24 @@ import { AuthRequest } from '../../types/express.types.js';
export const getChecklist = async (req: Request, res: Response) => { export const getChecklist = async (req: Request, res: Response) => {
try { try {
const { applicationId, relocationId } = req.params; const { applicationId, relocationId } = req.params;
// Resolve human-readable applicationId (e.g. APP-2026-79CE90) to UUID
let resolvedAppId = applicationId as string;
if (applicationId) {
const appIdStr = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(appIdStr);
if (!isUUID) {
const app = await db.Application.findOne({ where: { applicationId: appIdStr } });
if (!app) {
res.status(404).json({ success: false, message: 'Application not found' });
return;
}
resolvedAppId = app.id;
}
}
let checklist = await EorChecklist.findOne({ let checklist = await EorChecklist.findOne({
where: relocationId ? { relocationId } : { applicationId }, where: relocationId ? { relocationId } : { applicationId: resolvedAppId },
// proofDocument is now polymorphic, would need manual stitch or sub-selects
include: [{ model: EorChecklistItem, as: 'items' }] include: [{ model: EorChecklistItem, as: 'items' }]
}); });
@ -47,29 +63,36 @@ export const getChecklist = async (req: Request, res: Response) => {
export const createChecklist = async (req: AuthRequest, res: Response) => { export const createChecklist = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId, relocationId } = req.body; const { applicationId: rawAppId, relocationId } = req.body;
if (!applicationId && !relocationId) { if (!rawAppId && !relocationId) {
return res.status(400).json({ success: false, message: 'applicationId or relocationId is required' }); return res.status(400).json({ success: false, message: 'applicationId or relocationId is required' });
} }
if (applicationId) { // Resolve applicationId to UUID (handles readable IDs like APP-2026-79CE90)
const application = await db.Application.findByPk(applicationId); let resolvedAppId: string | null = null;
if (rawAppId) {
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(rawAppId);
const application = isUUID
? await db.Application.findByPk(rawAppId)
: await db.Application.findOne({ where: { applicationId: rawAppId } });
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
resolvedAppId = application.id;
} else if (relocationId) { } else if (relocationId) {
const relocation = await db.RelocationRequest.findByPk(relocationId); const relocation = await db.RelocationRequest.findByPk(relocationId);
if (!relocation) return res.status(404).json({ success: false, message: 'Relocation request not found' }); if (!relocation) return res.status(404).json({ success: false, message: 'Relocation request not found' });
} }
const [checklist, created] = await EorChecklist.findOrCreate({ const [checklist, created] = await EorChecklist.findOrCreate({
where: relocationId ? { relocationId } : { applicationId }, where: relocationId ? { relocationId } : { applicationId: resolvedAppId },
defaults: { defaults: {
status: 'In Progress', status: 'In Progress',
applicationId: applicationId || null, applicationId: resolvedAppId || null,
relocationId: relocationId || null relocationId: relocationId || null
} }
}); });
if (created) { if (created) {
// Define Default Mandatory Items per SRS/Frontend // Define Default Mandatory Items per SRS/Frontend
let defaultItems = []; let defaultItems = [];
@ -111,6 +134,30 @@ export const createChecklist = async (req: AuthRequest, res: Response) => {
})); }));
await EorChecklistItem.bulkCreate(itemsData); await EorChecklistItem.bulkCreate(itemsData);
// AUTO-MAP existing documents from OnboardingDocument table
if (resolvedAppId) {
const existingDocs = await OnboardingDocument.findAll({
where: { applicationId: resolvedAppId, status: 'active' }
});
if (existingDocs.length > 0) {
const typeMap: any = {
'GST Certificate': 'GST certificate including Accessories & Apparels billing',
'Virtual Code Confirmation': 'Virtual code availability',
'Trade Certificate': 'Trade certificate with test ride bikes registration',
'DMS Infra Details': 'DMS infra'
};
for (const doc of existingDocs) {
const targetDescription = typeMap[doc.documentType] || doc.documentType;
await EorChecklistItem.update(
{ proofDocumentId: doc.id },
{ where: { checklistId: checklist.id, description: { [Op.iLike]: targetDescription.trim() } } }
);
}
}
}
} }
// Status transition will be handled by the global handleApprove workflow or explicit trigger // Status transition will be handled by the global handleApprove workflow or explicit trigger

View File

@ -1,4 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Op } from 'sequelize';
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';
@ -8,8 +9,20 @@ import { WorkflowService } from '../../services/WorkflowService.js';
export const getAssignment = async (req: Request, res: Response) => { export const getAssignment = async (req: Request, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
const targetId = applicationId as string;
// Resolve application first to get UUID
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
const assignment = await FddAssignment.findOne({ const assignment = await FddAssignment.findOne({
where: { applicationId }, where: { applicationId: application.id },
include: [{ model: FddReport, as: 'reports' }] include: [{ model: FddReport, as: 'reports' }]
}); });
res.json({ success: true, data: assignment }); res.json({ success: true, data: assignment });
@ -22,22 +35,30 @@ export const getAssignment = async (req: Request, res: Response) => {
export const assignAgency = async (req: AuthRequest, res: Response) => { export const assignAgency = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId, assignedToAgency } = req.body; const { applicationId, assignedToAgency } = req.body;
const targetId = applicationId as string;
// Resolve application first to get UUID
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
const assignment = await FddAssignment.create({ const assignment = await FddAssignment.create({
applicationId, applicationId: application.id,
assignedToAgency, // Agency User ID assignedToAgency, // Agency User ID
status: 'Assigned' status: 'Assigned'
}); });
// Bridge: Transition application to active FDD stage // Bridge: Transition application to active FDD stage
const application = await Application.findByPk(applicationId); await WorkflowService.transitionApplication(application, APPLICATION_STATUS.FDD_VERIFICATION, req.user?.id || null, {
if (application) { reason: 'FDD Agency assigned. Initiating financial due diligence.',
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.FDD_VERIFICATION, req.user?.id || null, { stage: APPLICATION_STAGES.FDD,
reason: 'FDD Agency assigned. Initiating financial due diligence.', progressPercentage: 70
stage: APPLICATION_STAGES.FDD, });
progressPercentage: 70
});
}
await AuditLog.create({ await AuditLog.create({
userId: req.user?.id, userId: req.user?.id,
@ -72,70 +93,7 @@ export const uploadReport = async (req: AuthRequest, res: Response) => {
{ where: { id: assignmentId } } { where: { id: assignmentId } }
); );
// Transition Application status (AUTOMATION) res.status(201).json({ success: true, message: 'FDD Report uploaded successfully. Pending Admin review.', data: report });
const assignmentRecord = await FddAssignment.findByPk(assignmentId);
if (assignmentRecord) {
const application = await Application.findByPk(assignmentRecord.applicationId);
if (application) {
// 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 || [])}`);
}
}
res.status(201).json({ success: true, message: 'FDD Report uploaded', data: report });
} catch (error) { } catch (error) {
console.error('Upload FDD report error:', error); console.error('Upload FDD report error:', error);
res.status(500).json({ success: false, message: 'Error uploading report' }); res.status(500).json({ success: false, message: 'Error uploading report' });

View File

@ -26,8 +26,20 @@ const ensureLoaPolicy = async () => {
export const getRequest = async (req: Request, res: Response) => { export const getRequest = async (req: Request, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
// Resolve application first to get UUID
const application = await db.Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
const request = await LoaRequest.findOne({ const request = await LoaRequest.findOne({
where: { applicationId }, where: { applicationId: application.id },
include: [ include: [
{ model: LoaApproval, as: 'approvals' }, { model: LoaApproval, as: 'approvals' },
{ model: LoaDocumentGenerated, as: 'generatedDocuments' } { model: LoaDocumentGenerated, as: 'generatedDocuments' }
@ -43,12 +55,17 @@ export const getRequest = async (req: Request, res: Response) => {
export const createRequest = async (req: AuthRequest, res: Response) => { export const createRequest = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId } = req.body; const { applicationId } = req.body;
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await db.Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
const application = await db.Application.findByPk(applicationId);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
const [request, created] = await LoaRequest.findOrCreate({ const [request, created] = await LoaRequest.findOrCreate({
where: { applicationId }, where: { applicationId: application.id },
defaults: { defaults: {
requestedBy: req.user?.id, requestedBy: req.user?.id,
status: 'In Progress' status: 'In Progress'
@ -176,8 +193,8 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
const application = await db.Application.findByPk(request.applicationId); const application = await db.Application.findByPk(request.applicationId);
if (application) { if (application) {
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOA_ISSUED, req.user.id, { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.EOR_IN_PROGRESS, req.user.id, {
reason: 'LOA fully approved and issued', reason: 'LOA fully approved. Moving to EOR Work.',
progressPercentage: 97 progressPercentage: 97
}); });
} }
@ -205,9 +222,20 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
export const getApprovalStatus = async (req: AuthRequest, res: Response) => { export const getApprovalStatus = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await db.Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
const policy = await ensureLoaPolicy(); const policy = await ensureLoaPolicy();
const actions = await StageApprovalAction.findAll({ const actions = await StageApprovalAction.findAll({
where: { applicationId, stageCode: LOA_STAGE_CODE }, where: { applicationId: application.id, stageCode: LOA_STAGE_CODE },
include: [{ model: User, as: 'actor', attributes: ['id', 'fullName', 'email', 'roleCode'] }], include: [{ model: User, as: 'actor', attributes: ['id', 'fullName', 'email', 'roleCode'] }],
order: [['updatedAt', 'DESC']] order: [['updatedAt', 'DESC']]
}); });
@ -285,55 +313,15 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) =>
}); });
// --- AUTOMATION: After verification transitions --- // --- AUTOMATION: After verification transitions ---
// 1. If INITIAL Payment Verified -> Approve LOI Finance Role // 1. If INITIAL Payment Verified -> Move to LOI Issue Stage
// 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') { if ((depositType === 'INITIAL' || !depositType) && status === 'Verified') {
console.log(`[DEBUG] Initial Deposit VERIFIED for Application: ${application.id}. Ensuring LOI records exist...`); console.log(`[DEBUG] Initial Security Deposit verified. Moving to LOI Issued stage...`);
const LoiRequest = db.LoiRequest; await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_ISSUED, req.user?.id || null, {
const LoiApproval = db.LoiApproval; reason: 'Initial Security Deposit verified. Proceeding to LOI Issuance.',
stage: APPLICATION_STAGES.LOI,
// 1. Proactively ensure the LOI Request exists if the payment is cleared progressPercentage: 80
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. 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,
actionedAt: new Date(),
comments: 'Initial Security Deposit verified.'
});
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
});
} else {
console.log(`[DEBUG] No pending Finance approval in LOI stage. Skipping auto-bridge.`);
}
} }
// 2. If FINAL Payment Verified -> Move to LOA Pending stage // 2. If FINAL Payment Verified -> Move to LOA Pending stage

View File

@ -12,20 +12,41 @@ const ensureLoiPolicy = async () => {
where: { stageCode: LOI_STAGE_CODE }, where: { stageCode: LOI_STAGE_CODE },
defaults: { defaults: {
stageCode: LOI_STAGE_CODE, stageCode: LOI_STAGE_CODE,
minApprovals: 3, minApprovals: 2,
approvalMode: 'ROLE_MANDATORY', approvalMode: 'ROLE_MANDATORY',
requiredRoles: ['Finance', 'DD Head', 'NBH'], requiredRoles: ['DD Head', 'NBH'],
isActive: true isActive: true
} }
}); });
// If policy already exists but has Finance, update it
if (policy && Array.isArray(policy.requiredRoles) && policy.requiredRoles.includes('Finance')) {
await policy.update({
requiredRoles: ['DD Head', 'NBH'],
minApprovals: 2
});
}
return policy; return policy;
}; };
export const getRequest = async (req: Request, res: Response) => { export const getRequest = async (req: Request, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
// Resolve application first to get UUID
const application = await db.Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
const request = await LoiRequest.findOne({ const request = await LoiRequest.findOne({
where: { applicationId }, where: { applicationId: application.id },
include: [ include: [
{ model: LoiApproval, as: 'approvals' }, { model: LoiApproval, as: 'approvals' },
{ model: LoiDocumentGenerated, as: 'generatedDocuments' }, { model: LoiDocumentGenerated, as: 'generatedDocuments' },
@ -73,33 +94,38 @@ export const acknowledgeRequest = async (req: AuthRequest, res: Response) => {
export const createRequest = async (req: AuthRequest, res: Response) => { export const createRequest = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId } = req.body; const { applicationId } = req.body;
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await db.Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
const application = await db.Application.findByPk(applicationId);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
const [request, created] = await LoiRequest.findOrCreate({ const [request, created] = await LoiRequest.findOrCreate({
where: { applicationId }, where: { applicationId: application.id },
defaults: { defaults: {
requestedBy: req.user?.id, requestedBy: req.user?.id,
status: 'In Progress' status: 'In Progress'
} }
}); });
// Initialize first level approval (Finance) if not already exists // Initialize first level approval (DD Head) if not already exists
await LoiApproval.findOrCreate({ await LoiApproval.findOrCreate({
where: { requestId: request.id, level: 1 }, where: { requestId: request.id, level: 1 },
defaults: { defaults: {
approverRole: 'Finance', approverRole: 'DD Head',
action: 'Pending' action: 'Pending'
} }
}); });
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_IN_PROGRESS, req.user?.id || null, { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_IN_PROGRESS, req.user?.id || null, {
reason: 'LOI Request initiated with Finance approval', reason: 'LOI Request initiated for DD Head approval',
progressPercentage: 75 progressPercentage: 75
}); });
res.status(201).json({ success: true, message: 'LOI Request initiated with Finance approval', data: request }); res.status(201).json({ success: true, message: 'LOI Request initiated for DD Head approval', data: request });
} catch (error) { } catch (error) {
console.error('Create LOI request error:', error); console.error('Create LOI request error:', error);
res.status(500).json({ success: false, message: 'Error creating LOI request' }); res.status(500).json({ success: false, message: 'Error creating LOI request' });
@ -260,9 +286,20 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
export const getApprovalStatus = async (req: AuthRequest, res: Response) => { export const getApprovalStatus = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await db.Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
const policy = await ensureLoiPolicy(); const policy = await ensureLoiPolicy();
const actions = await StageApprovalAction.findAll({ const actions = await StageApprovalAction.findAll({
where: { applicationId, stageCode: LOI_STAGE_CODE }, where: { applicationId: application.id, stageCode: LOI_STAGE_CODE },
include: [{ model: User, as: 'actor', attributes: ['id', 'fullName', 'email', 'roleCode'] }], include: [{ model: User, as: 'actor', attributes: ['id', 'fullName', 'email', 'roleCode'] }],
order: [['updatedAt', 'DESC']] order: [['updatedAt', 'DESC']]
}); });

View File

@ -264,7 +264,7 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
'GST Certificate', 'PAN Card', 'Bank Statement', 'Cancelled Check', 'GST Certificate', 'PAN Card', 'Bank Statement', 'Cancelled Check',
'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA',
'Property Documents', 'Rental Agreement', 'Firm Registration', 'CIBIL Report', 'Property Documents', 'Rental Agreement', 'Firm Registration', 'CIBIL Report',
'FDD Final Audit Report', 'FDD Audit Report' 'FDD Final Audit Report', 'FDD Audit Report', 'Income Tax Returns (ITR)', 'Business Valuation Report'
]; ];
if (restrictedData.uploadedDocuments) { if (restrictedData.uploadedDocuments) {
@ -302,17 +302,21 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
export const updateApplicationStatus = async (req: AuthRequest, res: Response) => { export const updateApplicationStatus = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const targetId = id as string;
const { status, stage, reason } = req.body; const { status, stage, reason } = req.body;
const application = await Application.findByPk(id); // Resolve application by ID (UUID) or Registeration Number (applicationId)
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
if (application) { await WorkflowService.transitionApplication(application, status, req.user?.id || null, {
await WorkflowService.transitionApplication(application, status, req.user?.id || null, { reason: reason || 'Manual Status Update',
reason: reason || 'Manual Status Update', stage: stage
stage: stage });
});
}
res.json({ success: true, message: 'Application status updated successfully' }); res.json({ success: true, message: 'Application status updated successfully' });
} catch (error) { } catch (error) {
@ -335,13 +339,10 @@ export const uploadDocuments = async (req: any, res: Response) => {
return res.status(400).json({ success: false, message: 'Document type is required' }); return res.status(400).json({ success: false, message: 'Document type is required' });
} }
const targetId = id as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({ const application = await Application.findOne({
where: { where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
[Op.or]: [
{ id },
{ applicationId: id }
]
}
}); });
if (!application) { if (!application) {
@ -402,11 +403,19 @@ export const uploadDocuments = async (req: any, res: Response) => {
await db.EorChecklistItem.bulkCreate(itemsData); await db.EorChecklistItem.bulkCreate(itemsData);
} }
// Update the matching item - Link only, don't auto-verify (requested by user) console.log(`[debug] EOR Checklist found/created: ${checklist.id} for Application: ${application.id}`);
await db.EorChecklistItem.update(
const [updatedCount] = await db.EorChecklistItem.update(
{ proofDocumentId: newDoc.id, isCompliant: false }, { proofDocumentId: newDoc.id, isCompliant: false },
{ where: { checklistId: checklist.id, description: documentType } } {
where: {
checklistId: checklist.id,
description: { [Op.iLike]: documentType.trim() }
}
}
); );
console.log(`[debug] EOR items updated: ${updatedCount} for type: ${documentType}`);
} }
res.status(201).json({ res.status(201).json({
@ -424,14 +433,12 @@ export const getApplicationDocuments = async (req: AuthRequest, res: Response) =
try { try {
const { id } = req.params; const { id } = req.params;
const targetId = id as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
// Resolve ID to primary key if it's an appId string // Resolve ID to primary key if it's an appId string
const application = await Application.findOne({ const application = await Application.findOne({
where: { where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
[Op.or]: [
{ id },
{ applicationId: id }
]
}
}); });
if (!application) { if (!application) {
@ -484,8 +491,9 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
// Update Applications sequentially via WorkflowService for consistency // Update Applications sequentially via WorkflowService for consistency
for (const appId of applicationIds) { for (const appId of applicationIds) {
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(appId);
const application = await Application.findOne({ const application = await Application.findOne({
where: { [Op.or]: [{ id: appId }, { applicationId: appId }] } where: isUUID ? { [Op.or]: [{ id: appId }, { applicationId: appId }] } : { applicationId: appId }
}); });
if (application) { if (application) {
await application.update({ await application.update({
@ -541,8 +549,9 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
const assignStageEvaluators = async (appIdOrId: string) => { const assignStageEvaluators = async (appIdOrId: string) => {
try { try {
console.log(`[debug] Starting stage evaluator assignment for App: ${appIdOrId}`); console.log(`[debug] Starting stage evaluator assignment for App: ${appIdOrId}`);
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(appIdOrId);
const application = await Application.findOne({ const application = await Application.findOne({
where: { [Op.or]: [{ id: appIdOrId }, { applicationId: appIdOrId }] }, where: isUUID ? { [Op.or]: [{ id: appIdOrId }, { applicationId: appIdOrId }] } : { applicationId: appIdOrId },
include: [ include: [
{ {
model: District, model: District,
@ -693,14 +702,18 @@ const assignStageEvaluators = async (appIdOrId: string) => {
export const retriggerEvaluators = async (req: AuthRequest, res: Response) => { export const retriggerEvaluators = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const application = await Application.findByPk(id); const targetId = id as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
// Remove existing auto-mapped participants (Interviews, LOI, LOA) // Remove existing auto-mapped participants (Interviews, LOI, LOA)
// Using a more robust Postgres-compatible JSON path check // Using a more robust Postgres-compatible JSON path check
await db.RequestParticipant.destroy({ await db.RequestParticipant.destroy({
where: { where: {
requestId: id, requestId: application.id,
requestType: 'application', requestType: 'application',
joinedMethod: 'auto', joinedMethod: 'auto',
[Op.and]: [ [Op.and]: [
@ -726,6 +739,7 @@ export const retriggerEvaluators = async (req: AuthRequest, res: Response) => {
export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => { export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const targetId = id as string;
const { userId, assignedTo, remarks } = req.body; const { userId, assignedTo, remarks } = req.body;
const targetUserId = userId || assignedTo; const targetUserId = userId || assignedTo;
@ -733,7 +747,10 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) =>
return res.status(400).json({ success: false, message: 'Architecture team member (userId) is required' }); return res.status(400).json({ success: false, message: 'Architecture team member (userId) is required' });
} }
const application = await Application.findByPk(id); const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
await application.update({ await application.update({
@ -768,9 +785,13 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) =>
export const updateArchitectureStatus = async (req: AuthRequest, res: Response) => { export const updateArchitectureStatus = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const targetId = id as string;
const { status, remarks } = req.body; const { status, remarks } = req.body;
const application = await Application.findByPk(id); const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
const updateData: any = { const updateData: any = {
@ -805,8 +826,12 @@ import { ExternalMocksService } from '../../common/utils/externalMocks.service.j
export const generateDealerCodes = async (req: AuthRequest, res: Response) => { export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; // applicationId const { id } = req.params; // applicationId
const targetId = id as string;
const application = await Application.findByPk(id); const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
// Trigger Mock SAP Integration // Trigger Mock SAP Integration
@ -815,7 +840,7 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
// Save Dealer Codes // Save Dealer Codes
await db.DealerCode.create({ await db.DealerCode.create({
dealerCode: sapData.salesCode, // Use sales code as primary dealer code dealerCode: sapData.salesCode, // Use sales code as primary dealer code
applicationId: id, applicationId: application.id,
salesCode: sapData.salesCode, salesCode: sapData.salesCode,
serviceCode: sapData.serviceCode, serviceCode: sapData.serviceCode,
gmaCode: sapData.gmaCode, gmaCode: sapData.gmaCode,
@ -827,7 +852,7 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
// Create Final Security Deposit record (Blocker for LOA) // Create Final Security Deposit record (Blocker for LOA)
await db.SecurityDeposit.findOrCreate({ await db.SecurityDeposit.findOrCreate({
where: { applicationId: id, depositType: 'FINAL' }, where: { applicationId: application.id, depositType: 'FINAL' },
defaults: { defaults: {
amount: 1500000, // 15 Lakhs Final amount: 1500000, // 15 Lakhs Final
status: 'Pending' status: 'Pending'
@ -959,7 +984,12 @@ export const deleteDocumentConfig = async (req: AuthRequest, res: Response) => {
export const updateApplication = async (req: AuthRequest, res: Response) => { export const updateApplication = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const application = await Application.findByPk(id); const targetId = id as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
await application.update(req.body); await application.update(req.body);

View File

@ -85,8 +85,14 @@ export const submitResponse = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId, responses } = req.body; // responses: [{ questionId, value }] const { applicationId, responses } = req.body; // responses: [{ questionId, value }]
const targetId = applicationId as string;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
// Verify application // Verify application
const application = await Application.findByPk(applicationId); const application = await Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
});
if (!application) { if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' }); return res.status(404).json({ success: false, message: 'Application not found' });
} }
@ -96,7 +102,7 @@ export const submitResponse = async (req: AuthRequest, res: Response) => {
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' }); if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
const responseRecords = responses.map((r: any) => ({ const responseRecords = responses.map((r: any) => ({
applicationId, applicationId: application.id,
questionnaireId: questionnaire.id, questionnaireId: questionnaire.id,
questionId: r.questionId, questionId: r.questionId,
responseValue: r.value, responseValue: r.value,