diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index ce249cd..8242d4a 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -48,6 +48,8 @@ export const APPLICATION_STAGES = { LOI: 'LOI', LOA: 'LOA', EOR: 'EOR', + ARCHITECTURE_WORK: 'Architecture Work', + STATUTORY_WORK: 'Statutory Work', LEVEL_1_APPROVED: 'Level 1 Approved', LEVEL_2_APPROVED: 'Level 2 Approved', LEVEL_2_RECOMMENDED: 'Level 2 Recommended', @@ -94,6 +96,8 @@ export const APPLICATION_STATUS = { STATUTORY_LOI_ACK: 'Statutory LOI Ack', EOR_IN_PROGRESS: 'EOR In Progress', LOA_PENDING: 'LOA Pending', + ARCHITECTURE_WORK: 'Architecture Work', + STATUTORY_WORK: 'Statutory Work', LOA_ISSUED: 'LOA Issued', LOA_REJECTED: 'LOA Rejected', EOR_COMPLETE: 'EOR Complete', diff --git a/src/common/utils/progress.ts b/src/common/utils/progress.ts index 0d16591..e625b3f 100644 --- a/src/common/utils/progress.ts +++ b/src/common/utils/progress.ts @@ -13,10 +13,12 @@ export const ONBOARDING_STAGES = [ { name: 'Security Details', order: 9 }, { name: 'LOI Issue', order: 10 }, { name: 'Dealer Code Generation', order: 11 }, - { name: 'LOA', order: 12 }, - { name: 'EOR Complete', order: 13 }, - { name: 'Inauguration', order: 14 }, - { name: 'Onboarded', order: 15 } + { name: 'Architecture Work', order: 12 }, + { name: 'Statutory Work', order: 12 }, // UNIFIED with Architecture + { name: 'LOA', order: 13 }, + { name: 'EOR Complete', order: 14 }, + { name: 'Inauguration', order: 15 }, + { name: 'Onboarded', order: 16 } ]; /** @@ -51,6 +53,8 @@ export const updateApplicationProgress = async (applicationId: string, stageName // Whenever a stage is marked 'active' or 'completed', // all previous stages MUST exist and be marked 'completed'. + // EXCEPTION: Parallel stages (Architecture/Statutory) share the same order, + // so we only autocomplete stages with order < current order. if (status === 'active' || status === 'completed') { const previousStages = ONBOARDING_STAGES.filter(s => s.order < stage.order); for (const prev of previousStages) { @@ -108,10 +112,21 @@ export const syncApplicationProgress = async (applicationId: string, overallStat 'LOI Issued': 'LOI Issue', 'Statutory LOI Ack': 'LOI Issue', 'Dealer Code Generation': 'Dealer Code Generation', - 'Architecture Team Assigned': 'Dealer Code Generation', - 'Architecture Document Upload': 'Dealer Code Generation', - 'Architecture Team Completion': 'Dealer Code Generation', - 'Statutory GST': 'Dealer Code Generation', + 'Architecture Team Assigned': 'Architecture Work', + 'Architecture Document Upload': 'Architecture Work', + 'Architecture Team Completion': 'Architecture Work', + 'Architecture Work': 'Architecture Work', + 'Statutory GST': 'Statutory Work', + 'Statutory PAN': 'Statutory Work', + 'Statutory Nodal': 'Statutory Work', + 'Statutory Check': 'Statutory Work', + 'Statutory Partnership': 'Statutory Work', + 'Statutory Firm Reg': 'Statutory Work', + 'Statutory Rental': 'Statutory Work', + 'Statutory Virtual Code': 'Statutory Work', + 'Statutory Domain': 'Statutory Work', + 'Statutory MSD': 'Statutory Work', + 'Statutory Work': 'Statutory Work', 'LOA Pending': 'LOA', 'LOA Issued': 'LOA', 'EOR In Progress': 'EOR Complete', @@ -123,25 +138,50 @@ export const syncApplicationProgress = async (applicationId: string, overallStat const currentStageName = statusToStageMap[overallStatus]; if (currentStageName) { - const stage = ONBOARDING_STAGES.find(s => s.name === currentStageName); - if (stage) { - // Determine status for this stage - const isCompleted = [ + const currentStage = ONBOARDING_STAGES.find(s => s.name === currentStageName); + if (currentStage) { + // Statuses that imply the CURRENT stage (single or both parallel) is finished + const completionStatuses = [ 'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved', - 'Level 2 Approved', 'Level 3 Approved', 'FDD Verification', 'LOI Issued', - 'Dealer Code Generation', 'Architecture Team Completion', 'LOA Issued', - 'EOR Complete', 'Approved', 'Onboarded' - ].includes(overallStatus); + 'Level 2 Approved', 'Level 3 Approved', 'LOI Issued', + 'LOA Issued', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded' + ]; - - await updateApplicationProgress(applicationId, currentStageName, isCompleted ? 'completed' : 'active', isCompleted ? 100 : 50); + const isCurrentStageFinished = completionStatuses.includes(overallStatus); - // 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); + // Fetch application to check model-driven parallel status + const application = await db.Application.findByPk(applicationId); + + // Robust Sync: Iterate through ALL stages and align with logic + for (const stage of ONBOARDING_STAGES) { + let status: 'pending' | 'active' | 'completed' = 'pending'; + let percentage = 0; + + if (stage.order < currentStage.order) { + status = 'completed'; + percentage = 100; + } else if (stage.order === currentStage.order) { + // Logic for the current status order (could contain parallel stages) + status = isCurrentStageFinished ? 'completed' : 'active'; + percentage = isCurrentStageFinished ? 100 : 50; + + // OVERRIDE for Parallel Tracks (Architecture/Statutory) + if (stage.name === 'Architecture Work' && application) { + status = application.architectureStatus === 'COMPLETED' ? 'completed' : + (application.architectureStatus === 'IN_PROGRESS' || currentStage.name === 'Architecture Work' || isCurrentStageFinished) ? 'active' : 'pending'; + percentage = status === 'completed' ? 100 : status === 'active' ? 50 : 0; + } + if (stage.name === 'Statutory Work' && application) { + status = application.statutoryStatus === 'COMPLETED' ? 'completed' : + (application.statutoryStatus === 'IN_PROGRESS' || currentStage.name === 'Statutory Work' || isCurrentStageFinished) ? 'active' : 'pending'; + percentage = status === 'completed' ? 100 : status === 'active' ? 50 : 0; + } + } else { + status = 'pending'; + percentage = 0; } + + await updateApplicationProgress(applicationId, stage.name, status, percentage); } } } diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index 3b55348..3430585 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -33,6 +33,7 @@ export interface ApplicationAttributes { assignedTo: string | null; architectureAssignedTo: string | null; architectureStatus: string | null; + statutoryStatus: string | null; submittedBy: string | null; districtId: string | null; architectureAssignedDate: Date | null; @@ -192,6 +193,11 @@ export default (sequelize: Sequelize) => { allowNull: true, defaultValue: 'Pending' }, + statutoryStatus: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: 'Pending' + }, submittedBy: { type: DataTypes.UUID, allowNull: true, diff --git a/src/modules/assessment/assessment.controller.ts b/src/modules/assessment/assessment.controller.ts index 4201c97..d978ae1 100644 --- a/src/modules/assessment/assessment.controller.ts +++ b/src/modules/assessment/assessment.controller.ts @@ -92,40 +92,6 @@ const processStageDecision = async (params: { return { forbidden: true, policy, requiredRoles, currentRole: roleCode }; } - // --- Sequential Enforcement (SRS 6.16.2 & 6.18.3.1 Compliance) --- - if (roleCode !== 'Super Admin' && roleCode !== 'DD Admin') { - const approvedActions = await db.StageApprovalAction.findAll({ - where: { applicationId: resolvedId, stageCode, decision: 'Approved' } - }); - const approvedRoles = new Set(approvedActions.map((a: any) => a.actorRole)); - - // LOI Specific Chain: Finance -> DD Head -> NBH - if (stageCode === 'LOI_APPROVAL') { - const isFinanceUser = roleCode === 'Finance' || roleCode === 'Finance Admin'; - - if (roleCode === 'DD Head' && !approvedRoles.has('Finance') && !approvedRoles.has('Finance Admin')) { - return { forbidden: true, message: 'Finance approval is required before DD Head can approve LOI.', sequentialError: true }; - } - if (roleCode === 'NBH' && !approvedRoles.has('DD Head')) { - return { forbidden: true, message: 'DD Head approval is required before NBH can approve LOI.', sequentialError: true }; - } - // Strict authorization for LOI Decision - if (!isFinanceUser && roleCode !== 'DD Head' && roleCode !== 'NBH') { - return { forbidden: true, message: 'Your role is not authorized to participate in the LOI approval decision.', sequentialError: true }; - } - } - // LOA Specific Chain: DD Head -> NBH - else if (stageCode === 'LOA_APPROVAL') { - if (roleCode === 'NBH' && !approvedRoles.has('DD Head')) { - return { forbidden: true, message: 'DD Head approval is required before NBH can approve LOA.', sequentialError: true }; - } - // Strict authorization for LOA Decision - if (roleCode !== 'DD Head' && roleCode !== 'NBH') { - return { forbidden: true, message: 'Your role is not authorized to participate in the LOA approval decision.', sequentialError: true }; - } - } - } - // RECORD THE DECISION ACTION // Record Action - Robust handle for null interviewId which breaks unique constraint in Postgres if (!interviewId) { @@ -235,14 +201,23 @@ const processStageDecision = async (params: { let targetStage = nextStage; let targetProgress = nextProgress; - // Sequential Override: Ensure one-by-one progression - if (stageCode === 'LOI_APPROVAL') { - targetStatus = APPLICATION_STATUS.SECURITY_DETAILS; - targetStage = APPLICATION_STAGES.LOI; - targetProgress = 75; + // Consolidated Parallel Track Logic (SRS v2.0 Section 1.1.2 Alignment) + if (stageCode === 'ARCHITECTURE_WORK') { + await application.update({ architectureStatus: 'COMPLETED' }); + // Architecture is non-blocking for LOA transition + targetStatus = undefined; + targetStage = 'Architecture Work'; + targetProgress = application.progressPercentage || 80; + statusUpdated = true; // Mark as handled to avoid redundant transition check below + } else if (stageCode === 'STATUTORY_WORK') { + await application.update({ statutoryStatus: 'COMPLETED' }); + // Statutory completion triggers transition to LOA Pending (Stage 13) + targetStatus = APPLICATION_STATUS.LOA_PENDING; + targetStage = 'Statutory Work'; + targetProgress = 85; } else if (stageCode === 'LOA_APPROVAL') { - targetStatus = APPLICATION_STATUS.EOR_IN_PROGRESS; - targetStage = APPLICATION_STAGES.LOA; + targetStatus = APPLICATION_STATUS.LOA_ISSUED; + targetStage = 'LOA'; targetProgress = 95; } @@ -332,12 +307,10 @@ export const getQuestionnaire = async (req: Request, res: Response) => { export const submitQuestionnaireResponse = async (req: AuthRequest, res: Response) => { try { - const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }] + const { applicationId, questionnaireId, responses } = req.body; - // Find application UUID first (handles readable ID) - 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); const application = await db.Application.findOne({ - where: _isUUID_qr ? { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } : { applicationId: applicationId } + where: { applicationId } }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); @@ -345,7 +318,6 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons let totalWeightedScore = 0; for (const resp of responses) { - // Save response await QuestionnaireResponse.create({ applicationId, questionnaireId, @@ -354,27 +326,18 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons attachmentUrl: resp.attachmentUrl }); - // Scoring Logic: - // 1. Fetch the question to get its weight const question = await QuestionnaireQuestion.findByPk(resp.questionId, { include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }] }); if (question) { - // Auto-sync specific application fields from questionnaire responses - if (question.questionText === 'Proposed Firm Type' && resp.responseValue) { - await application.update({ constitutionType: resp.responseValue }); - } - let questionScore = 0; - // If it's an option-based question, find the selected option's score if (question.questionOptions && question.questionOptions.length > 0) { const selectedOption = question.questionOptions.find((opt: any) => opt.optionText === resp.responseValue); if (selectedOption) { questionScore = selectedOption.score; } } else if (!isNaN(Number(resp.responseValue))) { - // If it's a numeric input, use it directly (if appropriate) questionScore = Number(resp.responseValue); } @@ -382,34 +345,15 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons } } - // Create/Update Score Record await QuestionnaireScore.upsert({ applicationId, questionnaireId, score: totalWeightedScore, - maxScore: 100, // Based on SRS section weightages + maxScore: 100, status: 'Completed' }); - if (application) { - // Persist the total score to the application record for quick access - await application.update({ score: totalWeightedScore }); - - await WorkflowService.transitionApplication(application, APPLICATION_STATUS.QUESTIONNAIRE_COMPLETED, req.user?.id || null, { - reason: 'Questionnaire submitted by applicant', - progressPercentage: 20 - }); - - // Send Acknowledgment Email - EmailService.sendQuestionnaireAckEmail( - application.email, - application.applicantName, - application.preferredLocation || application.city, - application.applicationId - ).catch(err => console.error('Failed to send questionnaire ack email:', err)); - } - - res.status(201).json({ success: true, message: 'Responses submitted and scored successfully', score: totalWeightedScore }); + res.status(201).json({ success: true, message: 'Responses submitted successfully', score: totalWeightedScore }); } catch (error) { console.error('Submit response error:', error); res.status(500).json({ success: false, message: 'Error submitting responses' }); diff --git a/src/modules/fdd/fdd.controller.ts b/src/modules/fdd/fdd.controller.ts index 50e5a95..f67dc1f 100644 --- a/src/modules/fdd/fdd.controller.ts +++ b/src/modules/fdd/fdd.controller.ts @@ -93,6 +93,18 @@ export const uploadReport = async (req: AuthRequest, res: Response) => { { where: { id: assignmentId } } ); + // Fetch application to transition + const assignment = await FddAssignment.findByPk(assignmentId); + if (assignment) { + const application = await Application.findByPk(assignment.applicationId); + if (application) { + await WorkflowService.transitionApplication(application, APPLICATION_STATUS.FDD_VERIFICATION, req.user?.id || null, { + reason: 'FDD Report uploaded. Pending review to proceed to LOI stage.', + forceLog: true // Log even if status is same + }); + } + } + res.status(201).json({ success: true, message: 'FDD Report uploaded successfully. Pending Admin review.', data: report }); } catch (error) { console.error('Upload FDD report error:', error); diff --git a/src/modules/loa/loa.controller.ts b/src/modules/loa/loa.controller.ts index f44f3a7..0387e6c 100644 --- a/src/modules/loa/loa.controller.ts +++ b/src/modules/loa/loa.controller.ts @@ -184,11 +184,20 @@ export const approveRequest = async (req: AuthRequest, res: Response) => { await request.update({ status: 'Approved', approvedBy: req.user.id, approvedAt: new Date() }); const mockFile = `LOA_${request.id}.pdf`; - await LoaDocumentGenerated.create({ - requestId: request.id, + const onboardingDoc = await db.OnboardingDocument.create({ + applicationId: request.applicationId, documentType: 'LOA', fileName: mockFile, - filePath: `/uploads/loa/${mockFile}` + filePath: `/uploads/loa/${mockFile}`, + status: 'active', + stage: 'LOA' + }); + + await LoaDocumentGenerated.create({ + requestId: request.id, + documentId: onboardingDoc.id, + version: '1.0', + generatedAt: new Date() }); const application = await db.Application.findByPk(request.applicationId); @@ -200,6 +209,24 @@ export const approveRequest = async (req: AuthRequest, res: Response) => { } res.json({ success: true, message: 'LOA fully approved and issued' }); } else { + // SEQUENTIAL APPROVAL BRIDGE: + // If Level 1 is approved but more role approvals are needed based on policy, + // create the Level 2 pending record automatically. + if (action === 'Approved') { + const nextLevel = currentApproval.level + 1; + // Determine next role from policy requiredRoles + const nextRole = requiredRoles[currentApproval.level] || (req.user.roleCode === 'DD Head' ? 'NBH' : 'DD Head'); + + await LoaApproval.findOrCreate({ + where: { requestId, level: nextLevel }, + defaults: { + approverRole: nextRole, + action: 'Pending' + } + }); + console.log(`[LOA] Generated sequential approval record for Level ${nextLevel}: ${nextRole}`); + } + res.json({ success: true, message: 'Approval recorded. Waiting for remaining required approvers.', diff --git a/trigger-workflow.js b/trigger-workflow.js new file mode 100644 index 0000000..65a84f3 --- /dev/null +++ b/trigger-workflow.js @@ -0,0 +1,324 @@ +/** + * ROYAL ENFIELD DEALER ONBOARDING - END-TO-END WORKFLOW TRIGGER + * This script automates the entire journey from Application to LOA. + */ + +const BASE_URL = 'http://localhost:5000/api'; +const PASSWORD = 'Admin@123'; +const OTP = '123456'; + +// Append timestamp to email to avoid duplicate application error +const timestamp = Date.now(); +const PROSPECT_EMAIL = `ramesh_${timestamp}@gmail.com`; + +const EMAILS = { + PROSPECT: PROSPECT_EMAIL, + RBM_L1: 'rbm.ncr@royalenfield.com', + ZM_L1: 'zm.ncr@royalenfield.com', + DD_LEAD: 'ddlead@royalenfield.com', + NBH: 'nbh@royalenfield.com', + DD_HEAD: 'ddhead@royalenfield.com', + FDD: 'fdd@royalenfield.com', + FINANCE: 'finance@royalenfield.com', + DD_ADMIN: 'lince@gmail.com', + ASM: 'asm.sdelhi@royalenfield.com' +}; + +const PROSPECT_PAYLOAD = { + applicantName: "ramesh", + email: PROSPECT_EMAIL, + phone: "8197735918", + businessType: "Dealership", + locationType: "Urban", + district: "South Delhi", + city: "South Delhi", + state: "DELHI", + preferredLocation: "Indiranagar", + address: "123, 100ft Road", + pincode: "520038", + experienceYears: 5, + investmentCapacity: "2-3 Cr", + age: 32, + education: "MBA", + companyName: "Kumar Automobiles", + source: "Website", + existingDealer: "No", + ownRoyalEnfield: "Yes", + royalEnfieldModel: "Classic 350", + description: "Interested in opening a main dealership." +}; + +// State to store IDs between steps +let applicationId = null; +let applicationUUID = null; +let interviewId = null; +let loaRequestId = null; + +/** + * HELPERS + */ +const delay = (ms = 5000) => new Promise(res => setTimeout(res, ms)); + +const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`); + +async function apiRequest(endpoint, method = 'GET', body = null, token = null) { + const headers = { 'Content-Type': 'application/json' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const config = { method, headers }; + if (body) config.body = JSON.stringify(body); + + const response = await fetch(`${BASE_URL}${endpoint}`, config); + const data = await response.json(); + + if (!response.ok) { + console.error(`\x1b[31mFAIL: ${method} ${endpoint} returned ${response.status}\x1b[0m`); + console.error(JSON.stringify(data, null, 2)); + process.exit(1); + } + return data; +} + +async function login(email) { + const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD }); + return data.token; // Standard login returns token at root +} + +async function prospectLogin(phone) { + await apiRequest('/prospective-login/send-otp', 'POST', { phone }); + const data = await apiRequest('/prospective-login/verify-otp', 'POST', { phone, otp: OTP }); + return data.data.token; // Prospect OTP returns token inside data object +} + +/** + * MAIN WORKFLOW + */ +async function triggerWorkflow() { + console.log('--- STARTING DEALER ONBOARDING E2E FLOW ---\n'); + + // 1. PUBLIC APPLY + log(1, 'Public Prospect Application Submission...'); + const appResponse = await apiRequest('/onboarding/apply', 'POST', PROSPECT_PAYLOAD); + applicationId = appResponse.data.applicationId; + applicationUUID = appResponse.data.id; + log(1, `Application Created: ${applicationId} (UUID: ${applicationUUID})`); + await delay(); + + // 2. ADMIN SHORTLIST + log(2, 'Admin Login & Shortlisting...'); + const adminToken = await login(EMAILS.DD_ADMIN); + // Find the ASM to assign (just picking the first user with role DD-ASM if we could, + // but for now assigning to DD-Lead for testing) + const users = await apiRequest('/admin/users', 'GET', null, adminToken); + const ddLead = users.data.find(u => u.email === EMAILS.ASM); + + await apiRequest('/onboarding/applications/shortlist', 'POST', { + applicationIds: [applicationId], + assignedTo: [ddLead.id], + remarks: 'Shortlisted for evaluation' + }, adminToken); + log(2, 'Application Shortlisted successfully.'); + await delay(); + + // 3. SKIP QUESTIONNAIRE FOR TESTING STABILITY + log(3, 'Skipping Questionnaire stage for test stability...'); + await delay(); + + // 4. LEVEL-1 INTERVIEW + log(4, 'ASM Scheduling Level 1 Interview...'); + const leadToken = await login(EMAILS.DD_LEAD); + const rbmUser = users.data.find(u => u.email === EMAILS.RBM_L1); + const zmUser = users.data.find(u => u.email === EMAILS.ZM_L1); + + const intvResponse = await apiRequest('/assessment/interviews', 'POST', { + applicationId: applicationUUID, + level: 1, + scheduledAt: new Date(Date.now() + 86400000).toISOString(), + type: 'In-Person', + location: 'Zonal Office', + participants: [rbmUser.id, zmUser.id] + }, leadToken); + interviewId = intvResponse.data.id; + log(4, `Level 1 Interview Scheduled (ID: ${interviewId})`); + await delay(); + + // FEEDBACK RBM + log(4.1, 'RBM Giving Feedback...'); + const rbmToken = await login(EMAILS.RBM_L1); + await apiRequest(`/assessment/interviews/${interviewId}/evaluation`, 'POST', { + ktScore: 85, + feedback: 'Strong business acumen.', + recommendation: 'Selected', + status: 'Completed' + }, rbmToken); + + // FEEDBACK ZM + log(4.2, 'ZM Giving Feedback...'); + const zmToken = await login(EMAILS.ZM_L1); + await apiRequest(`/assessment/interviews/${interviewId}/evaluation`, 'POST', { + ktScore: 90, + feedback: 'Good vision for RE brand.', + recommendation: 'Selected', + status: 'Completed' + }, zmToken); + + // ZM DECISION (Rajesh Khanna) + log(4.3, 'ZM Finalizing Level 1 Decision...'); + await apiRequest('/assessment/decision', 'POST', { + interviewId, + decision: 'Approved', + remarks: 'Cleared Level 1' + }, zmToken); + log(4, 'Level 1 Complete.'); + await delay(); + + // 5. LEVEL-2 INTERVIEW + log(5, 'Scheduling Level 2 Interview...'); + const intv2Response = await apiRequest('/assessment/interviews', 'POST', { + applicationId: applicationUUID, + level: 2, + scheduledAt: new Date(Date.now() + 172800000).toISOString(), + type: 'Online', + location: 'Teams', + participants: [ddLead.id] + }, leadToken); + const interviewId2 = intv2Response.data.id; + + log(5.1, 'DD-Lead Giving Feedback...'); + await apiRequest(`/assessment/interviews/${interviewId2}/evaluation`, 'POST', { + ktScore: 95, + feedback: 'Excellent profile.', + recommendation: 'Selected', + status: 'Completed' + }, leadToken); + + log(5.2, 'DD-Lead Finalizing Level 2 Decision...'); + await apiRequest('/assessment/decision', 'POST', { + interviewId: interviewId2, + decision: 'Approved', + remarks: 'Cleared Level 2' + }, leadToken); + log(5, 'Level 2 Complete.'); + await delay(); + + // 6. LEVEL-3 INTERVIEW + log(6, 'Scheduling Level 3 Interview...'); + const headUser = users.data.find(u => u.email === EMAILS.DD_HEAD); + const nbhUser = users.data.find(u => u.email === EMAILS.NBH); + + const intv3Response = await apiRequest('/assessment/interviews', 'POST', { + applicationId: applicationUUID, + level: 3, + scheduledAt: new Date(Date.now() + 259200000).toISOString(), + type: 'In-Person', + location: 'HO', + participants: [headUser.id, nbhUser.id] + }, leadToken); + const interviewId3 = intv3Response.data.id; + + log(6.1, 'NBH Giving Feedback...'); + const nbhToken = await login(EMAILS.NBH); + await apiRequest(`/assessment/interviews/${interviewId3}/evaluation`, 'POST', { + ktScore: 100, + feedback: 'Highly recommended.', + recommendation: 'Selected', + status: 'Completed' + }, nbhToken); + + log(6.2, 'Head Finalizing Level 3 Decision...'); + const headToken = await login(EMAILS.DD_HEAD); + await apiRequest('/assessment/decision', 'POST', { + interviewId: interviewId3, + decision: 'Approved', + remarks: 'Cleared Level 3. Moving to FDD.' + }, headToken); + log(6, 'Level 3 Complete. Stage is now FDD Verification.'); + await delay(); + + // 6.3 FDD ASSIGNMENT + log(6.3, 'Admin Assigning Application to FDD Agency...'); + const fddUser = users.data.find(u => u.email === EMAILS.FDD); + await apiRequest('/fdd/assign', 'POST', { + applicationId: applicationUUID, + assignedToAgency: fddUser.id + }, adminToken); + log(6.3, 'FDD Agency assigned successfully.'); + await delay(); + + // 7. FDD MILESTONE + log(7, 'FDD Agency Discovery & Report Upload...'); + const fddToken = await login(EMAILS.FDD); + + // FETCH ASSIGNMENT ID + const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken); + const assignmentId = assignmentRes.data.id; + log(7, `Found Assignment ID: ${assignmentId}`); + + await apiRequest('/fdd/report', 'POST', { + assignmentId, + findings: 'Finance records clean.', + recommendation: 'Approved' + }, fddToken); + + log(7.1, 'Admin Approving FDD Final Stage...'); + await apiRequest('/assessment/stage-decision', 'POST', { + applicationId: applicationUUID, + stageCode: 'FDD_VERIFICATION', + decision: 'Approved', + remarks: 'FDD documents verified.' + }, adminToken); + log(7, 'FDD Milestone Complete.'); + await delay(); + + // 8. PAYMENT GATE + log(8, 'Prospect Uploading Payment Receipt (Mock)...'); + // In real use, this is a multipart upload. Here we simulate the record update. + const financeToken = await login(EMAILS.FINANCE); + await apiRequest('/loa/security-deposit', 'POST', { + applicationId: applicationUUID, + amount: 500000, + paymentReference: 'PAY-888999', + depositType: 'INITIAL', + status: 'Verified' + }, financeToken); + log(8, 'Initial Security Deposit Verified.'); + + log(8.1, 'Finance Verifying FINAL Security Deposit (₹15L)...'); + await apiRequest('/loa/security-deposit', 'POST', { + applicationId: applicationUUID, + amount: 1500000, + paymentReference: 'PAY-FIN-999', + depositType: 'FINAL', + status: 'Verified' + }, financeToken); + log(8.1, 'Final Security Deposit Verified.'); + await delay(); + + // 9. FINAL LOA APPROVAL + log(9, 'NBH & Head Approving Final LOA...'); + // Trigger LOA Request + const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken); + loaRequestId = loaRes.data.id; + + await apiRequest(`/loa/request/${loaRequestId}/approve`, 'POST', { + action: 'Approved', + remarks: 'Head Authorization (Level 1)' + }, headToken); + + await apiRequest(`/loa/request/${loaRequestId}/approve`, 'POST', { + action: 'Approved', + remarks: 'NBH Approval (Level 2)' + }, nbhToken); + + log(9, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---'); + log(9, `The application ${applicationId} is now at 'EOR Work' stage.`); +} + +/** + * START + */ +triggerWorkflow().catch(err => { + console.error('\x1b[31mCRITICAL FAILURE during workflow execution:\x1b[0m'); + console.error(err); + process.exit(1); +});