From 37ecf3ba8507172624cce80f9766eed0d4d79747 Mon Sep 17 00:00:00 2001 From: laxman h Date: Tue, 7 Apr 2026 20:29:59 +0530 Subject: [PATCH] documents type added and progress track sequence is justifie for LOI stage paralle to sequence --- package.json | 3 +- scripts/seed-document-configs.ts | 146 +++++++++++++++++ src/common/config/constants.ts | 12 ++ src/common/utils/progress.ts | 2 +- src/database/models/DocumentStageConfig.ts | 66 ++++++++ src/database/models/QuestionnaireScore.ts | 29 +++- src/database/models/index.ts | 2 + .../assessment/assessment.controller.ts | 125 +++++++++++---- src/modules/dealer/dealer.controller.ts | 2 +- .../onboarding/onboarding.controller.ts | 149 ++++++++++++++++-- src/modules/onboarding/onboarding.routes.ts | 10 +- src/scripts/check-interviews.ts | 27 ++++ src/scripts/check-participants.ts | 23 +++ 13 files changed, 546 insertions(+), 50 deletions(-) create mode 100644 scripts/seed-document-configs.ts create mode 100644 src/database/models/DocumentStageConfig.ts create mode 100644 src/scripts/check-interviews.ts create mode 100644 src/scripts/check-participants.ts diff --git a/package.json b/package.json index a84bede..3e01ad8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "seed:approval-policies": "tsx scripts/seed-approval-policies.ts", "seed:email-templates": "tsx src/scripts/seed-master-emails.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", "seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy", "sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts", diff --git a/scripts/seed-document-configs.ts b/scripts/seed-document-configs.ts new file mode 100644 index 0000000..e08fd47 --- /dev/null +++ b/scripts/seed-document-configs.ts @@ -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)); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index c046d89..e467c65 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -406,3 +406,15 @@ export const REQUEST_TYPES = { CONSTITUTIONAL: 'constitutional', RELOCATION: 'relocation' } 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; diff --git a/src/common/utils/progress.ts b/src/common/utils/progress.ts index d13c7fb..acc4425 100644 --- a/src/common/utils/progress.ts +++ b/src/common/utils/progress.ts @@ -77,7 +77,7 @@ export const syncApplicationProgress = async (applicationId: string, overallStat // Map overallStatus to stage names const statusToStageMap: Record = { 'Submitted': 'Submitted', - 'Questionnaire Pending': 'Submitted', + 'Questionnaire Pending': 'Questionnaire', 'Questionnaire Completed': 'Questionnaire', 'Shortlisted': 'Shortlist', 'Level 1 Interview Pending': '1st Level Interview', diff --git a/src/database/models/DocumentStageConfig.ts b/src/database/models/DocumentStageConfig.ts new file mode 100644 index 0000000..89b5201 --- /dev/null +++ b/src/database/models/DocumentStageConfig.ts @@ -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 { } + +export default (sequelize: Sequelize) => { + const DocumentStageConfig = sequelize.define('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; +}; diff --git a/src/database/models/QuestionnaireScore.ts b/src/database/models/QuestionnaireScore.ts index 8b57a28..7aa08de 100644 --- a/src/database/models/QuestionnaireScore.ts +++ b/src/database/models/QuestionnaireScore.ts @@ -3,10 +3,13 @@ import { Model, DataTypes, Sequelize } from 'sequelize'; export interface QuestionnaireScoreAttributes { id: string; applicationId: string; - sectionName: string; + questionnaireId?: string; + sectionName?: string; score: number; - weightage: number; - weightedScore: number; + maxScore?: number; + status?: string; + weightage?: number; + weightedScore?: number; } export interface QuestionnaireScoreInstance extends Model, QuestionnaireScoreAttributes { } @@ -26,26 +29,38 @@ export default (sequelize: Sequelize) => { key: 'id' } }, + questionnaireId: { + type: DataTypes.UUID, + allowNull: true + }, sectionName: { type: DataTypes.STRING, - allowNull: false + allowNull: true }, score: { type: DataTypes.DECIMAL(10, 2), allowNull: false }, + maxScore: { + type: DataTypes.DECIMAL(10, 2), + allowNull: true + }, + status: { + type: DataTypes.STRING, + allowNull: true + }, weightage: { type: DataTypes.DECIMAL(10, 2), - allowNull: false + allowNull: true }, weightedScore: { type: DataTypes.DECIMAL(10, 2), - allowNull: false + allowNull: true } }, { tableName: 'questionnaire_scores', timestamps: true, - updatedAt: false + updatedAt: true }); (QuestionnaireScore as any).associate = (models: any) => { diff --git a/src/database/models/index.ts b/src/database/models/index.ts index bff3045..ba98659 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -23,6 +23,7 @@ import createSLAReminder from './SLAReminder.js'; import createSLAEscalationConfig from './SLAEscalationConfig.js'; import createWorkflowStageConfig from './WorkflowStageConfig.js'; import createSystemConfiguration from './SystemConfiguration.js'; +import createDocumentStageConfig from './DocumentStageConfig.js'; import createNotification from './Notification.js'; import createDistrict from './District.js'; import createLocation from './Location.js'; @@ -136,6 +137,7 @@ db.SLAReminder = createSLAReminder(sequelize); db.SLAEscalationConfig = createSLAEscalationConfig(sequelize); db.WorkflowStageConfig = createWorkflowStageConfig(sequelize); db.Notification = createNotification(sequelize); +db.DocumentStageConfig = createDocumentStageConfig(sequelize); db.District = createDistrict(sequelize); db.Location = createLocation(sequelize); db.Zone = createZone(sequelize); diff --git a/src/modules/assessment/assessment.controller.ts b/src/modules/assessment/assessment.controller.ts index 0f632dd..4e7fa47 100644 --- a/src/modules/assessment/assessment.controller.ts +++ b/src/modules/assessment/assessment.controller.ts @@ -82,6 +82,41 @@ const processStageDecision = async (params: { return { forbidden: true, policy, requiredRoles, currentRole: roleCode }; } + // --- Sequential Enforcement (SRS 6.16.2 & 6.18.3.1 Compliance) --- + if (roleCode !== 'Super Admin' && roleCode !== 'DD Admin') { + const approvedActions = await db.StageApprovalAction.findAll({ + where: { applicationId, stageCode, decision: 'Approved' } + }); + const approvedRoles = new Set(approvedActions.map((a: any) => a.actorRole)); + + // LOI Specific Chain: Finance -> DD Head -> NBH + if (stageCode === 'LOI_APPROVAL') { + const isFinanceUser = roleCode === 'Finance' || roleCode === 'Finance Admin'; + + if (roleCode === 'DD Head' && !approvedRoles.has('Finance') && !approvedRoles.has('Finance Admin')) { + return { forbidden: true, message: 'Finance approval is required before DD Head can approve LOI.', sequentialError: true }; + } + if (roleCode === 'NBH' && !approvedRoles.has('DD Head')) { + return { forbidden: true, message: 'DD Head approval is required before NBH can approve LOI.', sequentialError: true }; + } + // Strict authorization for LOI Decision + if (!isFinanceUser && roleCode !== 'DD Head' && roleCode !== 'NBH') { + return { forbidden: true, message: 'Your role is not authorized to participate in the LOI approval decision.', sequentialError: true }; + } + } + // LOA Specific Chain: DD Head -> NBH + else if (stageCode === 'LOA_APPROVAL') { + if (roleCode === 'NBH' && !approvedRoles.has('DD Head')) { + return { forbidden: true, message: 'DD Head approval is required before NBH can approve LOA.', sequentialError: true }; + } + // Strict authorization for LOA Decision + if (roleCode !== 'DD Head' && roleCode !== 'NBH') { + return { forbidden: true, message: 'Your role is not authorized to participate in the LOA approval decision.', sequentialError: true }; + } + } + } + + // RECORD THE DECISION ACTION // Record Action - Robust handle for null interviewId which breaks unique constraint in Postgres if (!interviewId) { const existing = await db.StageApprovalAction.findOne({ @@ -102,7 +137,7 @@ const processStageDecision = async (params: { } else { await db.StageApprovalAction.upsert({ applicationId, - interviewId, + interviewId: interviewId, stageCode, actorUserId: userId, actorRole: assignedRole || roleCode, @@ -238,8 +273,12 @@ export const getQuestionnaire = async (req: Request, res: Response) => { export const submitQuestionnaireResponse = async (req: AuthRequest, res: Response) => { try { const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }] - - const 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' }); let totalWeightedScore = 0; @@ -261,6 +300,11 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons }); if (question) { + // Auto-sync specific application fields from questionnaire responses + if (question.questionText === 'Proposed Firm Type' && resp.responseValue) { + await application.update({ constitutionType: resp.responseValue }); + } + let questionScore = 0; // If it's an option-based question, find the selected option's score if (question.questionOptions && question.questionOptions.length > 0) { @@ -287,6 +331,9 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons }); if (application) { + // Persist the total score to the application record for quick access + await application.update({ score: totalWeightedScore }); + await WorkflowService.transitionApplication(application, APPLICATION_STATUS.QUESTIONNAIRE_COMPLETED, req.user?.id || null, { reason: 'Questionnaire submitted by applicant', progressPercentage: 20 @@ -320,10 +367,16 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => { const levelNum = typeof level === 'string' ? parseInt(level.replace(/\D/g, ''), 10) : level; 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 const existingInterview = await Interview.findOne({ where: { - applicationId, + applicationId: application.id, level: levelNum || 1, status: { [Op.ne]: 'Cancelled' } } @@ -338,7 +391,7 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => { console.log('Creating Interview record...'); const interview = await Interview.create({ - applicationId, + applicationId: application.id, level: levelNum || 1, // Default to 1 if parsing fails scheduleDate: new Date(scheduledAt), interviewType: type, @@ -357,12 +410,9 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => { const newStatus = statusMap[levelNum] || 'Interview Scheduled'; - const application = await db.Application.findByPk(applicationId); - if (application) { - await WorkflowService.transitionApplication(application, newStatus, req.user?.id || null, { - reason: `Interview Level ${levelNum} Scheduled` - }); - } + await WorkflowService.transitionApplication(application, newStatus, req.user?.id || null, { + reason: `Interview Level ${levelNum} Scheduled` + }); // MOCK INTEGRATIONS // 1. Google Calendar Mock @@ -374,13 +424,10 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => { await interview.update({ linkOrLocation: meetLink }); // 2. WhatsApp Mock - const appRecord = await db.Application.findByPk(applicationId); - if (appRecord) { - await ExternalMocksService.mockSendWhatsApp( - appRecord.phone, - `Dear ${appRecord.applicantName}, your ${type} is scheduled at ${scheduledAt}. Join here: ${meetLink}` - ); - } + await ExternalMocksService.mockSendWhatsApp( + application.phone, + `Dear ${application.applicantName}, your ${type} is scheduled at ${scheduledAt}. Join here: ${meetLink}` + ); let participantIds: string[] = Array.isArray(participants) ? participants : []; @@ -388,7 +435,7 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => { if (participantIds.length === 0) { const preAssigned = await db.RequestParticipant.findAll({ where: { - requestId: applicationId, + requestId: application.id, requestType: 'application', 'metadata.interviewLevel': levelNum }, @@ -659,9 +706,16 @@ export const generateAiSummary = async (req: AuthRequest, res: Response) => { try { 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({ - where: { applicationId }, + where: { applicationId: app.id }, include: [{ model: InterviewEvaluation, as: 'evaluations' }] }); @@ -699,8 +753,15 @@ export const getAiSummary = async (req: Request, res: Response) => { try { 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({ - where: { applicationId }, + where: { applicationId: app.id }, order: [['createdAt', 'DESC']] }); @@ -718,8 +779,16 @@ export const getAiSummary = async (req: Request, res: Response) => { export const getInterviews = async (req: Request, res: Response) => { try { 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({ - where: { applicationId }, + where: { applicationId: app.id }, include: [ { model: InterviewParticipant, @@ -778,7 +847,7 @@ export const updateRecommendation = async (req: AuthRequest, res: Response) => { if (result.forbidden) { return res.status(403).json({ 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) { return res.status(403).json({ 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) { // Fallback: If no policy, just update application status directly (legacy behavior) if (nextStatus) { - const application = await db.Application.findByPk(applicationId); + const application = await db.Application.findOne({ + where: { [Op.or]: [{ id: applicationId }, { applicationId: applicationId }] } + }); if (application) { await WorkflowService.transitionApplication(application, nextStatus, req.user?.id || null, { reason: 'Fallback Transition (No Policy)', @@ -961,7 +1032,7 @@ export const submitStageDecision = async (req: AuthRequest, res: Response) => { if (result.forbidden) { return res.status(403).json({ 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}` }); } diff --git a/src/modules/dealer/dealer.controller.ts b/src/modules/dealer/dealer.controller.ts index 74fe8e4..c8d87f6 100644 --- a/src/modules/dealer/dealer.controller.ts +++ b/src/modules/dealer/dealer.controller.ts @@ -89,7 +89,7 @@ export const createDealer = async (req: AuthRequest, res: Response) => { dealerCodeId: targetDealerCodeId, legalName: application.applicantName, businessName: application.applicantName, - constitutionType: application.businessType, + constitutionType: application.constitutionType || 'Proprietorship', status: 'Active', onboardedAt: new Date() }); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index 18e4fed..bf5dd91 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -10,6 +10,8 @@ import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } f import { syncLocationManagers } from '../master/syncHierarchy.service.js'; import { WorkflowService } from '../../services/WorkflowService.js'; +const { DocumentStageConfig } = db; + // Helper to find district by name and state name combination const findDistrictByName = async (districtName: string, stateName?: string) => { if (!districtName) return null; @@ -30,7 +32,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { opportunityId, applicantName, email, phone, businessType, locationType, 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; // Check for duplicate application for SAME location @@ -84,7 +86,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => { state, experienceYears, 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, overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED, progressPercentage: isOpportunityAvailable ? 10 : 0, @@ -482,7 +484,9 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => { // Update Applications sequentially via WorkflowService for consistency 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) { await application.update({ ddLeadShortlisted: true, @@ -507,13 +511,13 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => { // Add all assigned users as participants for (const userId of assignedTo) { await db.RequestParticipant.findOrCreate({ - where: { requestId: appId, requestType: 'application', userId, participantType: 'assignee' }, + where: { requestId: application.id, requestType: 'application', userId, participantType: 'assignee' }, defaults: { joinedMethod: 'auto' } }); } // 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 */ -const assignStageEvaluators = async (applicationId: string) => { +const assignStageEvaluators = async (appIdOrId: string) => { try { - console.log(`[debug] Starting stage evaluator assignment for App: ${applicationId}`); - const application = await Application.findByPk(applicationId, { + console.log(`[debug] Starting stage evaluator assignment for App: ${appIdOrId}`); + const application = await Application.findOne({ + where: { [Op.or]: [{ id: appIdOrId }, { applicationId: appIdOrId }] }, include: [ { model: District, @@ -551,12 +556,12 @@ const assignStageEvaluators = async (applicationId: string) => { }); if (!application) { - console.log(`[debug] Application ${applicationId} not found`); + console.log(`[debug] Application ${appIdOrId} not found`); return; } 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; } @@ -645,7 +650,7 @@ const assignStageEvaluators = async (applicationId: string) => { const [participant, created] = await db.RequestParticipant.findOrCreate({ where: { - requestId: applicationId, + requestId: application.id, requestType: 'application', userId: userId }, @@ -680,7 +685,7 @@ const assignStageEvaluators = async (applicationId: string) => { } } } 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' }); } }; +// 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' }); + } +}; diff --git a/src/modules/onboarding/onboarding.routes.ts b/src/modules/onboarding/onboarding.routes.ts index a927ed0..1cd68a6 100644 --- a/src/modules/onboarding/onboarding.routes.ts +++ b/src/modules/onboarding/onboarding.routes.ts @@ -4,7 +4,8 @@ import { submitApplication, getApplications, getApplicationById, updateApplicationStatus, uploadDocuments, getApplicationDocuments, bulkShortlist, assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes, - retriggerEvaluators + retriggerEvaluators, getDocumentConfigs, getDocumentConfigMetadata, + createDocumentConfig, updateDocumentConfig, deleteDocumentConfig, updateApplication } from './onboarding.controller.js'; import { authenticate } from '../../common/middleware/auth.js'; @@ -17,9 +18,16 @@ router.post('/apply', submitApplication); // All subsequent routes require authentication 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('/document-configs/metadata', getDocumentConfigMetadata); +router.get('/document-configs', getDocumentConfigs); router.post('/applications/shortlist', bulkShortlist); // Existing route, updated to named import router.get('/applications/:id', getApplicationById); +router.put('/applications/:id', updateApplication); router.put('/applications/:id/status', updateApplicationStatus); router.post('/applications/:id/documents', uploadSingle, uploadDocuments); router.get('/applications/:id/documents', getApplicationDocuments); // Existing route, updated to named import diff --git a/src/scripts/check-interviews.ts b/src/scripts/check-interviews.ts new file mode 100644 index 0000000..d3b2c1c --- /dev/null +++ b/src/scripts/check-interviews.ts @@ -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(); diff --git a/src/scripts/check-participants.ts b/src/scripts/check-participants.ts new file mode 100644 index 0000000..1768d09 --- /dev/null +++ b/src/scripts/check-participants.ts @@ -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();