import db from '../../database/models/index.js'; const { ApplicationProgress } = db; export const ONBOARDING_STAGES = [ { name: 'Submitted', order: 1 }, { name: 'Questionnaire', order: 2 }, { name: 'Shortlist', order: 3 }, { name: '1st Level Interview', order: 4 }, { name: '2nd Level Interview', order: 5 }, { name: '3rd Level Interview', order: 6 }, { name: 'FDD', order: 7 }, { name: 'LOI Approval', order: 8 }, { name: 'Security Details', order: 9 }, { name: 'LOI Issue', order: 10 }, { name: 'Dealer Code Generation', order: 11 }, { 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 } ]; /** * Updates application progress for a specific stage */ export const updateApplicationProgress = async (applicationId: string, stageName: string, status: 'pending' | 'active' | 'completed', percentage: number = 0) => { try { const stage = ONBOARDING_STAGES.find(s => s.name === stageName); if (!stage) return; const [progress, created] = await ApplicationProgress.findOrCreate({ where: { applicationId, stageName }, defaults: { stageOrder: stage.order, status, completionPercentage: percentage, stageStartedAt: status === 'active' ? new Date() : null, stageCompletedAt: status === 'completed' ? new Date() : null } }); if (!created) { const updates: any = { status, completionPercentage: percentage }; if (status === 'active' && !progress.stageStartedAt) { updates.stageStartedAt = new Date(); } if (status === 'completed' && !progress.stageCompletedAt) { updates.stageCompletedAt = new Date(); } await progress.update(updates); } // 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) { 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( { status: 'completed', completionPercentage: 100, stageCompletedAt: new Date() }, { where: { applicationId, stageOrder: { [db.Sequelize.Op.lt]: stage.order }, status: { [db.Sequelize.Op.ne]: 'completed' } } } ); } return progress; } catch (error) { console.error('Error updating application progress:', error); } }; /** * Maps application `overallStatus` to the pipeline stage label used in ApplicationProgress * and (via WorkflowService) `Application.currentStage` + audit `newData.stage`. * Keeps audit trail aligned with the post-LOI milestones (dealer code → LOA → EOR → inauguration). */ export const PIPELINE_STAGE_LABEL_BY_OVERALL_STATUS: Record = { Submitted: 'Submitted', 'Questionnaire Pending': 'Questionnaire', 'Questionnaire Completed': 'Questionnaire', Shortlisted: 'Shortlist', 'Level 1 Interview Pending': '1st Level Interview', 'Level 1 Approved': '1st Level Interview', 'Level 2 Interview Pending': '2nd Level Interview', 'Level 2 Approved': '2nd Level Interview', 'Level 2 Recommended': '2nd Level Interview', 'Level 3 Interview Pending': '3rd Level Interview', 'Level 3 Approved': '3rd Level Interview', 'FDD Verification': 'FDD', 'LOI In Progress': 'LOI Approval', 'Security Details': 'Security Details', 'Payment Pending': 'Security Details', 'LOI Issued': 'LOI Issue', 'Statutory LOI Ack': 'LOI Issue', 'Dealer Code Generation': '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', 'LOA Rejected': 'LOA', 'LOI Rejected': 'LOI Issue', 'EOR In Progress': 'EOR Complete', 'EOR Complete': 'EOR Complete', Inauguration: 'Inauguration', Approved: 'Inauguration', Onboarded: 'Onboarded', Rejected: 'Rejected', Disqualified: 'Disqualified', 'Returned to FDD': 'FDD', Pending: 'Submitted', 'In Review': 'Shortlist', }; /** * Syncs all progress stages based on current overall status */ export const syncApplicationProgress = async (applicationId: string, overallStatus: string) => { const currentStageName = PIPELINE_STAGE_LABEL_BY_OVERALL_STATUS[overallStatus]; if (currentStageName) { 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 2 Recommended', 'Level 3 Approved', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded' ]; const isCurrentStageFinished = completionStatuses.includes(overallStatus); // Fetch application to check model-driven parallel status const application = await db.Application.findByPk(applicationId); // Robust Sync: Prepare ALL stages for batch processing const upsertData: any[] = []; 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) { status = isCurrentStageFinished ? 'completed' : 'active'; percentage = isCurrentStageFinished ? 100 : 50; 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; } } upsertData.push({ applicationId, stageName: stage.name, stageOrder: stage.order, status, completionPercentage: percentage, stageStartedAt: (status === 'active' || status === 'completed') ? new Date() : null, stageCompletedAt: status === 'completed' ? new Date() : null }); } // DB Duplication Prevention without Schema Changes (Healing corrupted data loops) const existingRecords = await ApplicationProgress.findAll({ where: { applicationId } }); const seenStages = new Set(); // Purge any ghost duplicates created by old logic for (const record of existingRecords) { if (seenStages.has(record.stageName)) { await record.destroy(); } else { seenStages.add(record.stageName); } } // Perform single row updates/inserts to enforce exact 1:1 mapping safely const cleanedRecords = await ApplicationProgress.findAll({ where: { applicationId } }); for (const data of upsertData) { const existing = cleanedRecords.find((r: any) => r.stageName === data.stageName); if (existing) { await existing.update({ stageOrder: data.stageOrder, status: data.status, completionPercentage: data.completionPercentage, stageStartedAt: data.stageStartedAt || existing.stageStartedAt, stageCompletedAt: data.stageCompletedAt || existing.stageCompletedAt }); } else { await ApplicationProgress.create(data); } } } } };