documents type added and progress track sequence is justifie for LOI stage paralle to sequence

This commit is contained in:
laxman h 2026-04-07 20:29:59 +05:30
parent 8dbe83e230
commit 37ecf3ba85
13 changed files with 546 additions and 50 deletions

View File

@ -16,7 +16,8 @@
"seed:approval-policies": "tsx scripts/seed-approval-policies.ts", "seed:approval-policies": "tsx scripts/seed-approval-policies.ts",
"seed:email-templates": "tsx src/scripts/seed-master-emails.ts", "seed:email-templates": "tsx src/scripts/seed-master-emails.ts",
"seed:configs": "tsx scripts/seed-system-configs.ts", "seed:configs": "tsx scripts/seed-system-configs.ts",
"seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates && npm run seed:configs", "seed:document-configs": "tsx scripts/seed-document-configs.ts",
"seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates && npm run seed:configs && npm run seed:document-configs",
"setup:fresh": "npm run migrate && npm run seed:real-geo && npm run seed:all && npm run sync:hierarchy", "setup:fresh": "npm run migrate && npm run seed:real-geo && npm run seed:all && npm run sync:hierarchy",
"seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy", "seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy",
"sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts", "sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts",

View File

@ -0,0 +1,146 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
import { ROLES } from '../src/common/config/constants.js';
const { DocumentStageConfig } = db;
const ALL_ROLES = Object.values(ROLES);
const configs = [
// General / KYC Documents (Prospect/Dealer Initial)
{ documentType: 'PAN Card', stageCode: 'General', allowedRoles: ALL_ROLES, isMandatory: true },
{ documentType: 'GST Certificate', stageCode: 'General', allowedRoles: ALL_ROLES, isMandatory: true },
{ documentType: 'Aadhaar Card', stageCode: 'General', allowedRoles: ALL_ROLES, isMandatory: true },
{ documentType: 'Passport Size Photograph', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Partnership Deed', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'LLP Agreement', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Certificate of Incorporation', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Memorandum of Association (MOA)', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Articles of Association (AOA)', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Board Resolution', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Firm Registration Certificate', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Previous 6 Months Bank Statement', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'Cancelled Check', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'CIBIL Report (Self)', stageCode: 'General', allowedRoles: ALL_ROLES },
{ documentType: 'CIBIL Report (Firm)', stageCode: 'General', allowedRoles: ALL_ROLES },
// Assessment / Interview Recommendation Documents
{ documentType: 'KT Matrix Scorecard', stageCode: 'Level 1 Interview', allowedRoles: [ROLES.RBM, ROLES.ASM, ROLES.DD_ZM, ROLES.SUPER_ADMIN], isMandatory: true, module: 'ONBOARDING' },
{ documentType: 'Panel Interview Evaluation Sheet', stageCode: 'Level 2 Interview', allowedRoles: [ROLES.ZBH, ROLES.DD_LEAD, ROLES.SUPER_ADMIN], isMandatory: true, module: 'ONBOARDING' },
{ documentType: 'ZBH Recommendation Summary', stageCode: 'Level 2 Interview', allowedRoles: [ROLES.ZBH, ROLES.SUPER_ADMIN], module: 'ONBOARDING' },
{ documentType: 'Final Interview Recommendation Note', stageCode: 'Level 3 Interview', allowedRoles: [ROLES.NBH, ROLES.DD_HEAD, ROLES.SUPER_ADMIN], isMandatory: true, module: 'ONBOARDING' },
// FDD (Financial Due Diligence) Specific
{ documentType: 'FDD Final Audit Report', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.NBH], isMandatory: true },
{ documentType: 'FDD Agency Assignment Letter', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'Wealth Certificate', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'Net Worth Statement', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'ITR Returns (Last 3 Years)', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'Audited Balance Sheet', stageCode: 'FDD', allowedRoles: [ROLES.FDD, ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'Profit & Loss Statement', 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)
{ documentType: 'Initial 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: 'LOI Acknowledgement Copy', stageCode: 'LOI Issue', allowedRoles: ALL_ROLES },
{ documentType: 'Nodal Agreement', stageCode: 'LOI Approval', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.DEALER, ROLES.SUPER_ADMIN] },
// Architecture Team Documents
{ documentType: 'Architecture Assignment Document', stageCode: 'Architecture Team Assigned', allowedRoles: [ROLES.ARCHITECTURE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'Architecture Blueprint (Site Layout)', stageCode: 'Architecture Document Upload', allowedRoles: [ROLES.ARCHITECTURE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.NBH] },
{ documentType: 'Site Plan (2D/3D)', stageCode: 'Architecture Document Upload', allowedRoles: [ROLES.ARCHITECTURE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'Architecture Completion Certificate', stageCode: 'Architecture Team Completion', allowedRoles: [ROLES.ARCHITECTURE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.NBH] },
{ documentType: 'Pre-Construction Site Photos', stageCode: 'Architecture Team Assigned', allowedRoles: [ROLES.ARCHITECTURE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
{ documentType: 'Post-Construction Site Photos', stageCode: 'Architecture Team Completion', allowedRoles: [ROLES.ARCHITECTURE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN] },
// EOR (Essential Operating Requirements)
{ documentType: 'Rental Agreement / Lease Deed', stageCode: 'EOR', allowedRoles: ALL_ROLES, isMandatory: true },
{ documentType: 'Property Ownership Documents / Index II', stageCode: 'EOR', allowedRoles: ALL_ROLES },
{ documentType: 'Fire NOC', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN], isMandatory: true },
{ documentType: 'Shop & Establishment License (Gumastha)', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN], isMandatory: true },
{ documentType: 'Trade License', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN] },
{ documentType: 'Pollution Control Board Certificate', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN] },
{ documentType: 'Electricity Bill / Load Enhancement NOC', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN] },
{ documentType: 'Water Connection NOC / Bill', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN] },
{ documentType: 'Other Supporting Document', stageCode: 'General', allowedRoles: ALL_ROLES, module: 'ONBOARDING' },
{ documentType: 'Other Supporting Document', stageCode: 'General', allowedRoles: ALL_ROLES, module: 'RESIGNATION' },
{ documentType: 'Other Supporting Document', stageCode: 'General', allowedRoles: ALL_ROLES, module: 'RELOCATION' },
{ documentType: 'Other Supporting Document', stageCode: 'General', allowedRoles: ALL_ROLES, module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'Other Supporting Document', stageCode: 'General', allowedRoles: ALL_ROLES, module: 'TERMINATION' },
// Insurance Policy (Property & Stock) - EOR
{ documentType: 'Insurance Policy (Property & Stock)', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN], isMandatory: true, module: 'ONBOARDING' },
{ documentType: 'Workshop Tooling & Equipment Invoice', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN] },
{ documentType: 'Signage & Visual Branding Photos', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN, ROLES.NBH] },
{ documentType: 'DMS Access Request Form', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN] },
{ documentType: 'Local Authority Approvals', stageCode: 'EOR', allowedRoles: [ROLES.DEALER, ROLES.DD_ADMIN, ROLES.SUPER_ADMIN] },
// Final Steps
{ documentType: 'Inauguration Photos', stageCode: 'Inauguration', allowedRoles: ALL_ROLES },
{ documentType: 'Inauguration Report', stageCode: 'Inauguration', allowedRoles: ALL_ROLES },
// Fallback
// CONSTITUTIONAL_CHANGE Documents (Per ConstitutionalWorkflowService)
{ documentType: 'GST CERTIFICATE', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], isMandatory: true, module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'PAN CARD (OF PARTNERS/DIR)', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], isMandatory: true, module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'AADHAAR CARD (OF PARTNERS/DIR)', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'CANCELLED CHECK', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN, ROLES.FINANCE], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'PARTNERSHIP DEED', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'LLP AGREEMENT', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'MOA & AOA', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'CERTIFICATE OF INCORPORATION', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'BUSINESS PURCHASE AGREEMENT (BPA)', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'FIRM REGISTRATION CERTIFICATE', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
{ documentType: 'AUTHORIZATION LETTER / DECLARATION', stageCode: 'Legal Review', allowedRoles: [ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN], module: 'CONSTITUTIONAL_CHANGE' },
// EOR (Essential Operating Requirements) - Extended for Relocation & Onboarding Audit
{ documentType: 'SALES STANDARDS COMPLIANCE', stageCode: 'EOR', allowedRoles: ALL_ROLES, isMandatory: true, module: 'ONBOARDING' },
{ documentType: 'SERVICE & SPARES READINESS', stageCode: 'EOR', allowedRoles: ALL_ROLES, isMandatory: true, module: 'ONBOARDING' },
{ documentType: 'DMS INFRASTRUCTURE SETUP', stageCode: 'EOR', allowedRoles: [ROLES.DD_ADMIN, ROLES.SUPER_ADMIN, ROLES.ARCHITECTURE], isMandatory: true, module: 'ONBOARDING' },
{ documentType: 'MANPOWER TRAINING CERTIFICATES', stageCode: 'EOR', allowedRoles: [ROLES.DD_ADMIN, ROLES.SUPER_ADMIN], module: 'ONBOARDING' },
{ documentType: 'INVENTORY FUNDING APPROVAL', stageCode: 'EOR', allowedRoles: [ROLES.FINANCE, ROLES.SUPER_ADMIN], isMandatory: true, module: 'ONBOARDING' },
{ documentType: 'MARKETING & WEBSITE DETAILS', stageCode: 'EOR', allowedRoles: ALL_ROLES, module: 'ONBOARDING' },
{ documentType: 'BPA (BUSINESS PURCHASE AGREEMENT)', stageCode: 'EOR', allowedRoles: ALL_ROLES, module: 'ONBOARDING' },
// RELOCATION EOR Specific
{ documentType: 'NEW SITE LAYOUT / FLOOR PLAN', stageCode: 'EOR', allowedRoles: ALL_ROLES, isMandatory: true, module: 'RELOCATION' },
{ documentType: 'NOC FROM CURRENT LANDLORD', stageCode: 'EOR', allowedRoles: ALL_ROLES, isMandatory: true, module: 'RELOCATION' },
{ documentType: 'MUNICIPAL / FIRE SAFETY APPROVALS', stageCode: 'EOR', allowedRoles: ALL_ROLES, module: 'RELOCATION' },
{ documentType: 'LOCALITY MAP (LOCATION PIN)', stageCode: 'EOR', allowedRoles: ALL_ROLES, module: 'RELOCATION' },
// RESIGNATION Documents
{ documentType: 'RESIGNATION LETTER (SIGNED COPY)', stageCode: 'Submission', allowedRoles: [ROLES.DEALER, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN], isMandatory: true, module: 'RESIGNATION' },
{ documentType: 'ASSET HANDOVER CERTIFICATE', stageCode: 'ZBH Review', allowedRoles: [ROLES.DEALER, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN], isMandatory: true, module: 'RESIGNATION' },
{ documentType: 'NO DUES CERTIFICATE (FINANCE)', stageCode: 'Finance Review', allowedRoles: [ROLES.FINANCE, ROLES.SUPER_ADMIN], isMandatory: true, module: 'RESIGNATION' },
{ documentType: 'DEALER AGREEMENT TERMINATION DEED', stageCode: 'Approved', allowedRoles: [ROLES.SUPER_ADMIN, ROLES.LEGAL_ADMIN], module: 'RESIGNATION' },
// TERMINATION Documents
{ documentType: 'SHOW CAUSE NOTICE (SCN)', stageCode: 'Hearing', allowedRoles: [ROLES.SUPER_ADMIN, ROLES.LEGAL_ADMIN], isMandatory: true, module: 'TERMINATION' },
{ documentType: 'DEALER EXPLANATION / RESPONSE', stageCode: 'Hearing', allowedRoles: [ROLES.DEALER, ROLES.SUPER_ADMIN, ROLES.LEGAL_ADMIN], module: 'TERMINATION' },
{ documentType: 'FINAL TERMINATION ORDER', stageCode: 'Closed', allowedRoles: [ROLES.SUPER_ADMIN, ROLES.LEGAL_ADMIN], module: 'TERMINATION' }
];
async function seed() {
console.log('🌱 Updating Comprehensive Document Stage Configurations...');
// Ensure table structure is updated
await DocumentStageConfig.sync({ alter: true });
// Clear old configs to avoid confusion with the fixed amount labels
await DocumentStageConfig.destroy({ where: {} });
for (const config of configs) {
await DocumentStageConfig.create({
...config,
isActive: true
});
}
console.log('✅ Comprehensive document configs seeded!');
}
seed().catch(err => {
console.error(err);
process.exit(1);
}).then(() => process.exit(0));

View File

@ -406,3 +406,15 @@ export const REQUEST_TYPES = {
CONSTITUTIONAL: 'constitutional', CONSTITUTIONAL: 'constitutional',
RELOCATION: 'relocation' RELOCATION: 'relocation'
} as const; } as const;
// Module List for Document Management
export const MODULE_LIST = ['ONBOARDING', 'RESIGNATION', 'RELOCATION', 'CONSTITUTIONAL_CHANGE', 'TERMINATION'] as const;
// Process Stages per Module (Source of Truth for Checklists)
export const STAGES_MAP = {
'ONBOARDING': ['General', 'KYC', 'Level 1 Interview', 'Level 2 Interview', 'Level 3 Interview', 'FDD', 'LOI Approval', 'LOA Approval', 'LOI Issue', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'EOR', 'Inauguration'],
'RESIGNATION': ['Submission', 'Regional Review', 'ZM Review', 'ZBH Review', 'Finance Review', 'DDL Review', 'Approved'],
'RELOCATION': ['Initiated', 'ASM Review', 'ZM Review', 'ZBH Review', 'Completed'],
'CONSTITUTIONAL_CHANGE': ['Draft', 'Legal Review', 'Approved'],
'TERMINATION': ['Hearing', 'Review', 'Closed']
} as const;

View File

@ -77,7 +77,7 @@ export const syncApplicationProgress = async (applicationId: string, overallStat
// Map overallStatus to stage names // Map overallStatus to stage names
const statusToStageMap: Record<string, string> = { const statusToStageMap: Record<string, string> = {
'Submitted': 'Submitted', 'Submitted': 'Submitted',
'Questionnaire Pending': 'Submitted', 'Questionnaire Pending': 'Questionnaire',
'Questionnaire Completed': 'Questionnaire', 'Questionnaire Completed': 'Questionnaire',
'Shortlisted': 'Shortlist', 'Shortlisted': 'Shortlist',
'Level 1 Interview Pending': '1st Level Interview', 'Level 1 Interview Pending': '1st Level Interview',

View File

@ -0,0 +1,66 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface DocumentStageConfigAttributes {
id: string;
documentType: string;
stageCode: string;
allowedRoles: string[]; // Role codes allowed to view/upload
isMandatory: boolean;
module: string; // ONBOARDING, RESIGNATION, RELOCATION, etc.
description: string | null;
isActive: boolean;
}
export interface DocumentStageConfigInstance extends Model<DocumentStageConfigAttributes>, DocumentStageConfigAttributes { }
export default (sequelize: Sequelize) => {
const DocumentStageConfig = sequelize.define<DocumentStageConfigInstance>('DocumentStageConfig', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
documentType: {
type: DataTypes.STRING,
allowNull: false
},
stageCode: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'General'
},
allowedRoles: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: []
},
isMandatory: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
module: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'ONBOARDING'
}
}, {
tableName: 'document_stage_configs',
timestamps: true,
indexes: [
{ fields: ['stageCode'] },
{ fields: ['documentType'] },
{ fields: ['isActive'] },
{ fields: ['module'] }
]
});
return DocumentStageConfig;
};

View File

@ -3,10 +3,13 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface QuestionnaireScoreAttributes { export interface QuestionnaireScoreAttributes {
id: string; id: string;
applicationId: string; applicationId: string;
sectionName: string; questionnaireId?: string;
sectionName?: string;
score: number; score: number;
weightage: number; maxScore?: number;
weightedScore: number; status?: string;
weightage?: number;
weightedScore?: number;
} }
export interface QuestionnaireScoreInstance extends Model<QuestionnaireScoreAttributes>, QuestionnaireScoreAttributes { } export interface QuestionnaireScoreInstance extends Model<QuestionnaireScoreAttributes>, QuestionnaireScoreAttributes { }
@ -26,26 +29,38 @@ export default (sequelize: Sequelize) => {
key: 'id' key: 'id'
} }
}, },
questionnaireId: {
type: DataTypes.UUID,
allowNull: true
},
sectionName: { sectionName: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: true
}, },
score: { score: {
type: DataTypes.DECIMAL(10, 2), type: DataTypes.DECIMAL(10, 2),
allowNull: false allowNull: false
}, },
maxScore: {
type: DataTypes.DECIMAL(10, 2),
allowNull: true
},
status: {
type: DataTypes.STRING,
allowNull: true
},
weightage: { weightage: {
type: DataTypes.DECIMAL(10, 2), type: DataTypes.DECIMAL(10, 2),
allowNull: false allowNull: true
}, },
weightedScore: { weightedScore: {
type: DataTypes.DECIMAL(10, 2), type: DataTypes.DECIMAL(10, 2),
allowNull: false allowNull: true
} }
}, { }, {
tableName: 'questionnaire_scores', tableName: 'questionnaire_scores',
timestamps: true, timestamps: true,
updatedAt: false updatedAt: true
}); });
(QuestionnaireScore as any).associate = (models: any) => { (QuestionnaireScore as any).associate = (models: any) => {

View File

@ -23,6 +23,7 @@ import createSLAReminder from './SLAReminder.js';
import createSLAEscalationConfig from './SLAEscalationConfig.js'; import createSLAEscalationConfig from './SLAEscalationConfig.js';
import createWorkflowStageConfig from './WorkflowStageConfig.js'; import createWorkflowStageConfig from './WorkflowStageConfig.js';
import createSystemConfiguration from './SystemConfiguration.js'; import createSystemConfiguration from './SystemConfiguration.js';
import createDocumentStageConfig from './DocumentStageConfig.js';
import createNotification from './Notification.js'; import createNotification from './Notification.js';
import createDistrict from './District.js'; import createDistrict from './District.js';
import createLocation from './Location.js'; import createLocation from './Location.js';
@ -136,6 +137,7 @@ db.SLAReminder = createSLAReminder(sequelize);
db.SLAEscalationConfig = createSLAEscalationConfig(sequelize); db.SLAEscalationConfig = createSLAEscalationConfig(sequelize);
db.WorkflowStageConfig = createWorkflowStageConfig(sequelize); db.WorkflowStageConfig = createWorkflowStageConfig(sequelize);
db.Notification = createNotification(sequelize); db.Notification = createNotification(sequelize);
db.DocumentStageConfig = createDocumentStageConfig(sequelize);
db.District = createDistrict(sequelize); db.District = createDistrict(sequelize);
db.Location = createLocation(sequelize); db.Location = createLocation(sequelize);
db.Zone = createZone(sequelize); db.Zone = createZone(sequelize);

View File

@ -82,6 +82,41 @@ const processStageDecision = async (params: {
return { forbidden: true, policy, requiredRoles, currentRole: roleCode }; 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, 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 // 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({
@ -102,7 +137,7 @@ const processStageDecision = async (params: {
} else { } else {
await db.StageApprovalAction.upsert({ await db.StageApprovalAction.upsert({
applicationId, applicationId,
interviewId, interviewId: interviewId,
stageCode, stageCode,
actorUserId: userId, actorUserId: userId,
actorRole: assignedRole || roleCode, actorRole: assignedRole || roleCode,
@ -239,7 +274,11 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
try { try {
const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }] const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }]
const application = await db.Application.findByPk(applicationId); // Find application UUID first (handles readable ID)
const application = await db.Application.findOne({
where: { [Op.or]: [{ id: 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' });
let totalWeightedScore = 0; let totalWeightedScore = 0;
@ -261,6 +300,11 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
}); });
if (question) { 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; let questionScore = 0;
// If it's an option-based question, find the selected option's score // If it's an option-based question, find the selected option's score
if (question.questionOptions && question.questionOptions.length > 0) { if (question.questionOptions && question.questionOptions.length > 0) {
@ -287,6 +331,9 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
}); });
if (application) { 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, { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.QUESTIONNAIRE_COMPLETED, req.user?.id || null, {
reason: 'Questionnaire submitted by applicant', reason: 'Questionnaire submitted by applicant',
progressPercentage: 20 progressPercentage: 20
@ -320,10 +367,16 @@ 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({
where: { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] }
});
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
// Prevent duplicate interviews for the same level // Prevent duplicate interviews for the same level
const existingInterview = await Interview.findOne({ const existingInterview = await Interview.findOne({
where: { where: {
applicationId, applicationId: application.id,
level: levelNum || 1, level: levelNum || 1,
status: { [Op.ne]: 'Cancelled' } status: { [Op.ne]: 'Cancelled' }
} }
@ -338,7 +391,7 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
console.log('Creating Interview record...'); console.log('Creating Interview record...');
const interview = await Interview.create({ const interview = await Interview.create({
applicationId, applicationId: application.id,
level: levelNum || 1, // Default to 1 if parsing fails level: levelNum || 1, // Default to 1 if parsing fails
scheduleDate: new Date(scheduledAt), scheduleDate: new Date(scheduledAt),
interviewType: type, interviewType: type,
@ -357,12 +410,9 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
const newStatus = statusMap[levelNum] || 'Interview Scheduled'; const newStatus = statusMap[levelNum] || 'Interview Scheduled';
const application = await db.Application.findByPk(applicationId);
if (application) {
await WorkflowService.transitionApplication(application, newStatus, req.user?.id || null, { await WorkflowService.transitionApplication(application, newStatus, req.user?.id || null, {
reason: `Interview Level ${levelNum} Scheduled` reason: `Interview Level ${levelNum} Scheduled`
}); });
}
// MOCK INTEGRATIONS // MOCK INTEGRATIONS
// 1. Google Calendar Mock // 1. Google Calendar Mock
@ -374,13 +424,10 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
await interview.update({ linkOrLocation: meetLink }); await interview.update({ linkOrLocation: meetLink });
// 2. WhatsApp Mock // 2. WhatsApp Mock
const appRecord = await db.Application.findByPk(applicationId);
if (appRecord) {
await ExternalMocksService.mockSendWhatsApp( await ExternalMocksService.mockSendWhatsApp(
appRecord.phone, application.phone,
`Dear ${appRecord.applicantName}, your ${type} is scheduled at ${scheduledAt}. Join here: ${meetLink}` `Dear ${application.applicantName}, your ${type} is scheduled at ${scheduledAt}. Join here: ${meetLink}`
); );
}
let participantIds: string[] = Array.isArray(participants) ? participants : []; let participantIds: string[] = Array.isArray(participants) ? participants : [];
@ -388,7 +435,7 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
if (participantIds.length === 0) { if (participantIds.length === 0) {
const preAssigned = await db.RequestParticipant.findAll({ const preAssigned = await db.RequestParticipant.findAll({
where: { where: {
requestId: applicationId, requestId: application.id,
requestType: 'application', requestType: 'application',
'metadata.interviewLevel': levelNum 'metadata.interviewLevel': levelNum
}, },
@ -659,9 +706,16 @@ export const generateAiSummary = async (req: AuthRequest, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
// 1. Fetch all interview evaluations for this application // Find application UUID first
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(applicationId as string);
const app = await db.Application.findOne({
where: isUUID ? { [Op.or]: [{ id: applicationId }, { applicationId }] } : { applicationId }
});
if (!app) return res.status(404).json({ success: false, message: 'Application not found' });
// 1. Fetch all interview evaluations for this application using UUID
const interviews = await Interview.findAll({ const interviews = await Interview.findAll({
where: { applicationId }, where: { applicationId: app.id },
include: [{ model: InterviewEvaluation, as: 'evaluations' }] include: [{ model: InterviewEvaluation, as: 'evaluations' }]
}); });
@ -699,8 +753,15 @@ export const getAiSummary = async (req: Request, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
// Find application UUID first
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(applicationId as string);
const app = await db.Application.findOne({
where: isUUID ? { [Op.or]: [{ id: applicationId }, { applicationId }] } : { applicationId }
});
if (!app) return res.status(404).json({ success: false, message: 'Application not found' });
const summary = await AiSummary.findOne({ const summary = await AiSummary.findOne({
where: { applicationId }, where: { applicationId: app.id },
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
@ -718,8 +779,16 @@ export const getAiSummary = async (req: Request, res: Response) => {
export const getInterviews = async (req: Request, res: Response) => { export const getInterviews = async (req: Request, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
// Find application UUID first
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(applicationId as string);
const app = await db.Application.findOne({
where: isUUID ? { [Op.or]: [{ id: applicationId }, { applicationId }] } : { applicationId }
});
if (!app) return res.status(404).json({ success: false, message: 'Application not found' });
const interviews = await Interview.findAll({ const interviews = await Interview.findAll({
where: { applicationId }, where: { applicationId: app.id },
include: [ include: [
{ {
model: InterviewParticipant, model: InterviewParticipant,
@ -778,7 +847,7 @@ export const updateRecommendation = async (req: AuthRequest, res: Response) => {
if (result.forbidden) { if (result.forbidden) {
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: `Role ${result.currentRole} is not allowed to approve ${result.policy.stageCode}` message: result.message || `Role ${result.currentRole} is not allowed to approve ${result.policy?.stageCode || 'this stage'}`
}); });
} }
@ -820,7 +889,7 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) =
if (result.forbidden) { if (result.forbidden) {
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: `Role ${result.currentRole} is not allowed to approve ${result.policy.stageCode}` message: result.message || `Role ${result.currentRole} is not allowed to approve ${result.policy?.stageCode || 'this stage'}`
}); });
} }
@ -947,7 +1016,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.findByPk(applicationId); const application = await db.Application.findOne({
where: { [Op.or]: [{ id: 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, {
reason: 'Fallback Transition (No Policy)', reason: 'Fallback Transition (No Policy)',
@ -961,7 +1032,7 @@ export const submitStageDecision = async (req: AuthRequest, res: Response) => {
if (result.forbidden) { if (result.forbidden) {
return res.status(403).json({ return res.status(403).json({
success: false, success: false,
message: `Role ${result.currentRole} is not allowed to approve ${result.policy.stageCode}` message: result.message || `Role ${result.currentRole} is not allowed to approve ${result.policy?.stageCode || stageCode}`
}); });
} }

View File

@ -89,7 +89,7 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
dealerCodeId: targetDealerCodeId, dealerCodeId: targetDealerCodeId,
legalName: application.applicantName, legalName: application.applicantName,
businessName: application.applicantName, businessName: application.applicantName,
constitutionType: application.businessType, constitutionType: application.constitutionType || 'Proprietorship',
status: 'Active', status: 'Active',
onboardedAt: new Date() onboardedAt: new Date()
}); });

View File

@ -10,6 +10,8 @@ import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } f
import { syncLocationManagers } from '../master/syncHierarchy.service.js'; import { syncLocationManagers } from '../master/syncHierarchy.service.js';
import { WorkflowService } from '../../services/WorkflowService.js'; import { WorkflowService } from '../../services/WorkflowService.js';
const { DocumentStageConfig } = db;
// Helper to find district by name and state name combination // Helper to find district by name and state name combination
const findDistrictByName = async (districtName: string, stateName?: string) => { const findDistrictByName = async (districtName: string, stateName?: string) => {
if (!districtName) return null; if (!districtName) return null;
@ -30,7 +32,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
opportunityId, opportunityId,
applicantName, email, phone, businessType, locationType, applicantName, email, phone, businessType, locationType,
preferredLocation, city, state, experienceYears, investmentCapacity, preferredLocation, city, state, experienceYears, investmentCapacity,
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, constitutionType
} = req.body; } = req.body;
// Check for duplicate application for SAME location // Check for duplicate application for SAME location
@ -84,7 +86,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
state, state,
experienceYears, experienceYears,
investmentCapacity, investmentCapacity,
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, constitutionType,
currentStage: APPLICATION_STAGES.DD, currentStage: APPLICATION_STAGES.DD,
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED, overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
progressPercentage: isOpportunityAvailable ? 10 : 0, progressPercentage: isOpportunityAvailable ? 10 : 0,
@ -482,7 +484,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 application = await Application.findByPk(appId); const application = await Application.findOne({
where: { [Op.or]: [{ id: appId }, { applicationId: appId }] }
});
if (application) { if (application) {
await application.update({ await application.update({
ddLeadShortlisted: true, ddLeadShortlisted: true,
@ -507,13 +511,13 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
// Add all assigned users as participants // Add all assigned users as participants
for (const userId of assignedTo) { for (const userId of assignedTo) {
await db.RequestParticipant.findOrCreate({ await db.RequestParticipant.findOrCreate({
where: { requestId: appId, requestType: 'application', userId, participantType: 'assignee' }, where: { requestId: application.id, requestType: 'application', userId, participantType: 'assignee' },
defaults: { joinedMethod: 'auto' } defaults: { joinedMethod: 'auto' }
}); });
} }
// AUTO-FILL Interview Evaluators // AUTO-FILL Interview Evaluators
await assignStageEvaluators(appId); await assignStageEvaluators(application.id);
} }
} }
@ -534,10 +538,11 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
/** /**
* Helper to assign default evaluators for all 3 interview levels, LOI, and LOA based on location * Helper to assign default evaluators for all 3 interview levels, LOI, and LOA based on location
*/ */
const assignStageEvaluators = async (applicationId: string) => { const assignStageEvaluators = async (appIdOrId: string) => {
try { try {
console.log(`[debug] Starting stage evaluator assignment for App: ${applicationId}`); console.log(`[debug] Starting stage evaluator assignment for App: ${appIdOrId}`);
const application = await Application.findByPk(applicationId, { const application = await Application.findOne({
where: { [Op.or]: [{ id: appIdOrId }, { applicationId: appIdOrId }] },
include: [ include: [
{ {
model: District, model: District,
@ -551,12 +556,12 @@ const assignStageEvaluators = async (applicationId: string) => {
}); });
if (!application) { if (!application) {
console.log(`[debug] Application ${applicationId} not found`); console.log(`[debug] Application ${appIdOrId} not found`);
return; return;
} }
if (!application.district) { if (!application.district) {
console.log(`[debug] Application ${applicationId} has NO district linked. Skipping auto-assign.`); console.log(`[debug] Application ${appIdOrId} has NO district linked. Skipping auto-assign.`);
return; return;
} }
@ -645,7 +650,7 @@ const assignStageEvaluators = async (applicationId: string) => {
const [participant, created] = await db.RequestParticipant.findOrCreate({ const [participant, created] = await db.RequestParticipant.findOrCreate({
where: { where: {
requestId: applicationId, requestId: application.id,
requestType: 'application', requestType: 'application',
userId: userId userId: userId
}, },
@ -680,7 +685,7 @@ const assignStageEvaluators = async (applicationId: string) => {
} }
} }
} catch (error) { } catch (error) {
console.error(`Error assigning stage evaluators for application ${applicationId}:`, error); console.error(`Error assigning stage evaluators for application ${appIdOrId}:`, error);
} }
}; };
@ -844,3 +849,123 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
res.status(500).json({ success: false, message: 'Error generating dealer codes' }); res.status(500).json({ success: false, message: 'Error generating dealer codes' });
} }
}; };
// Fetch Metadata for Document Management (Modules & Stages)
export const getDocumentConfigMetadata = async (_req: AuthRequest, res: Response) => {
try {
const { MODULE_LIST, STAGES_MAP } = await import('../../common/config/constants.js');
res.json({
success: true,
data: {
modules: MODULE_LIST,
stages: STAGES_MAP
}
});
} catch (error) {
res.status(500).json({ success: false, message: 'Error fetching metadata' });
}
};
// Fetch Document Configurations based on Role and Stage
export const getDocumentConfigs = async (req: AuthRequest, res: Response) => {
try {
const { stageCode, search, page = 1, limit = 10, roleFilter, module = 'ONBOARDING' } = req.query;
const roleCode = (roleFilter as string) || req.user?.role;
const where: any = { module };
if (stageCode) {
where.stageCode = { [Op.or]: [stageCode, 'General'] };
}
if (search) {
where[Op.or] = [
{ documentType: { [Op.iLike]: `%${search}%` } },
{ stageCode: { [Op.iLike]: `%${search}%` } }
];
}
const offset = (Number(page) - 1) * Number(limit);
const { rows: configs, count } = await DocumentStageConfig.findAndCountAll({
where,
order: [['stageCode', 'DESC'], ['documentType', 'ASC']],
limit: Number(limit),
offset: Number(offset)
});
// Manual role filtering because it's a JSON field
// Note: For admin search, we might want to skip this
let filteredConfigs = configs;
if (roleCode && !req.query.isAdminView) {
filteredConfigs = configs.filter((c: any) => {
const allowedRoles = c.allowedRoles || [];
return allowedRoles.length === 0 || allowedRoles.includes(roleCode);
});
}
return res.json({
success: true,
data: filteredConfigs,
pagination: {
total: count,
page: Number(page),
limit: Number(limit),
pages: Math.ceil(count / Number(limit))
}
});
} catch (error) {
console.error('Failed to fetch document configs:', error);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
};
export const createDocumentConfig = async (req: AuthRequest, res: Response) => {
try {
const config = await DocumentStageConfig.create(req.body);
return res.json({ success: true, data: config });
} catch (error) {
console.error('Failed to create document config:', error);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
};
export const updateDocumentConfig = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const config = await DocumentStageConfig.findByPk(id);
if (!config) return res.status(404).json({ success: false, message: 'Config not found' });
await config.update(req.body);
return res.json({ success: true, data: config });
} catch (error) {
console.error('Failed to update document config:', error);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
};
export const deleteDocumentConfig = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const config = await DocumentStageConfig.findByPk(id);
if (!config) return res.status(404).json({ success: false, message: 'Config not found' });
await config.destroy();
return res.json({ success: true, message: 'Deleted successfully' });
} catch (error) {
console.error('Failed to delete document config:', error);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
};
export const updateApplication = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
await application.update(req.body);
return res.json({ success: true, message: 'Application updated successfully', data: application });
} catch (error) {
console.error('Failed to update application:', error);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
};

View File

@ -4,7 +4,8 @@ import {
submitApplication, getApplications, getApplicationById, updateApplicationStatus, submitApplication, getApplications, getApplicationById, updateApplicationStatus,
uploadDocuments, getApplicationDocuments, bulkShortlist, uploadDocuments, getApplicationDocuments, bulkShortlist,
assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes, assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes,
retriggerEvaluators retriggerEvaluators, getDocumentConfigs, getDocumentConfigMetadata,
createDocumentConfig, updateDocumentConfig, deleteDocumentConfig, updateApplication
} from './onboarding.controller.js'; } from './onboarding.controller.js';
import { authenticate } from '../../common/middleware/auth.js'; import { authenticate } from '../../common/middleware/auth.js';
@ -17,9 +18,16 @@ router.post('/apply', submitApplication);
// All subsequent routes require authentication // All subsequent routes require authentication
router.use(authenticate as any); router.use(authenticate as any);
router.post('/document-configs', createDocumentConfig);
router.put('/document-configs/:id', updateDocumentConfig);
router.delete('/document-configs/:id', deleteDocumentConfig);
router.get('/applications', getApplications); router.get('/applications', getApplications);
router.get('/document-configs/metadata', getDocumentConfigMetadata);
router.get('/document-configs', getDocumentConfigs);
router.post('/applications/shortlist', bulkShortlist); // Existing route, updated to named import router.post('/applications/shortlist', bulkShortlist); // Existing route, updated to named import
router.get('/applications/:id', getApplicationById); router.get('/applications/:id', getApplicationById);
router.put('/applications/:id', updateApplication);
router.put('/applications/:id/status', updateApplicationStatus); router.put('/applications/:id/status', updateApplicationStatus);
router.post('/applications/:id/documents', uploadSingle, uploadDocuments); router.post('/applications/:id/documents', uploadSingle, uploadDocuments);
router.get('/applications/:id/documents', getApplicationDocuments); // Existing route, updated to named import router.get('/applications/:id/documents', getApplicationDocuments); // Existing route, updated to named import

View File

@ -0,0 +1,27 @@
import db from '../database/models/index.js';
const checkInterviews = async () => {
try {
const interviews = await db.Interview.findAll({
include: [{
model: db.InterviewParticipant,
as: 'participants',
include: [{ model: db.User, as: 'user' }]
}]
});
console.log(`--- Interviews ---`);
interviews.forEach((i: any) => {
console.log(`ID: ${i.id}, AppUUID: ${i.applicationId}, Level: ${i.level}, Status: ${i.status}`);
i.participants?.forEach((p: any) => {
console.log(` - Participant: ${p.user?.name} (${p.user?.role}), Status: ${p.status}`);
});
});
process.exit(0);
} catch (err) {
console.error(err);
process.exit(1);
}
};
checkInterviews();

View File

@ -0,0 +1,23 @@
import db from '../database/models/index.js';
const checkParticipants = async () => {
try {
const participants = await db.InterviewParticipant.findAll();
console.log(`--- Participants Raw ---`);
participants.forEach((p: any) => {
console.log(`ID: ${p.id}, InterviewID: ${p.interviewId}, UserID: ${p.userId}, Status: ${p.status}`);
});
const users = await db.User.findAll();
console.log(`--- Users ---`);
users.forEach((u: any) => {
console.log(`ID: ${u.id}, Name: ${u.name}, Role: ${u.role}`);
});
process.exit(0);
} catch (err) {
console.error(err);
process.exit(1);
}
};
checkParticipants();