235 lines
10 KiB
TypeScript
235 lines
10 KiB
TypeScript
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<string, string> = {
|
|
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<string>();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|