documents type added and progress track sequence is justifie for LOI stage paralle to sequence
This commit is contained in:
parent
8dbe83e230
commit
37ecf3ba85
@ -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",
|
||||||
|
|||||||
146
scripts/seed-document-configs.ts
Normal file
146
scripts/seed-document-configs.ts
Normal 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));
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
66
src/database/models/DocumentStageConfig.ts
Normal file
66
src/database/models/DocumentStageConfig.ts
Normal 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;
|
||||||
|
};
|
||||||
@ -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) => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
27
src/scripts/check-interviews.ts
Normal file
27
src/scripts/check-interviews.ts
Normal 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();
|
||||||
23
src/scripts/check-participants.ts
Normal file
23
src/scripts/check-participants.ts
Normal 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();
|
||||||
Loading…
Reference in New Issue
Block a user