diff --git a/scripts/check_column.ts b/scripts/check_column.ts new file mode 100644 index 0000000..3de4626 --- /dev/null +++ b/scripts/check_column.ts @@ -0,0 +1,18 @@ +import db from '../src/database/models/index.js'; + +async function checkColumn() { + try { + const [results]: any = await db.sequelize.query(` + SELECT column_name, data_type, udt_name + FROM information_schema.columns + WHERE table_name = 'request_participants' AND column_name = 'participantType' + `); + console.log('Column definition:', results[0]); + } catch (error: any) { + console.error('Error fetching column:', error.message); + } finally { + process.exit(0); + } +} + +checkColumn(); diff --git a/scripts/check_enum.ts b/scripts/check_enum.ts new file mode 100644 index 0000000..d9fc088 --- /dev/null +++ b/scripts/check_enum.ts @@ -0,0 +1,19 @@ +import db from '../src/database/models/index.js'; + +async function checkEnum() { + try { + const [results]: any = await db.sequelize.query(` + SELECT enumlabel + FROM pg_enum + JOIN pg_type ON pg_enum.enumtypid = pg_type.oid + WHERE typname = 'enum_request_participants_participantType' + `); + console.log('Current enum values:', results.map((r: any) => r.enumlabel).join(', ')); + } catch (error: any) { + console.error('Error fetching enum:', error.message); + } finally { + process.exit(0); + } +} + +checkEnum(); diff --git a/scripts/test_enum_cast.ts b/scripts/test_enum_cast.ts new file mode 100644 index 0000000..58c1729 --- /dev/null +++ b/scripts/test_enum_cast.ts @@ -0,0 +1,26 @@ +import db from '../src/database/models/index.js'; + +async function testInsert() { + try { + // Attempt insert without checking existing records + // If it fails with "invalid input value", the enum is truly not updated. + // If it fails with "foreign key", the enum was VALID but the data was wrong. + await db.sequelize.query(` + DO $$ + BEGIN + -- This will fail if 'architecture' is invalid for the enum + PERFORM 'architecture'::"enum_request_participants_participantType"; + RAISE NOTICE '✅ Enum check passed!'; + EXCEPTION WHEN OTHERS THEN + RAISE EXCEPTION '❌ Enum check failed: %', SQLERRM; + END $$; + `); + console.log('✅ PL/pgSQL Enum check passed!'); + } catch (error: any) { + console.error(error.message); + } finally { + process.exit(0); + } +} + +testInsert(); diff --git a/scripts/test_insert.ts b/scripts/test_insert.ts new file mode 100644 index 0000000..05a1c84 --- /dev/null +++ b/scripts/test_insert.ts @@ -0,0 +1,22 @@ +import db from '../src/database/models/index.js'; + +async function testInsert() { + try { + const testId = '00000000-0000-0000-0000-000000000000'; + await db.sequelize.query(` + INSERT INTO request_participants ("id", "requestId", "requestType", "userId", "participantType", "joinedMethod", "createdAt", "updatedAt") + VALUES (gen_random_uuid(), '${testId}', 'test', '9950ee60-ddf6-4091-a1e6-e7161e6d8bb6', 'architecture', 'manual', now(), now()) + `); + console.log('✅ Manual insert successful!'); + + // Clean up + await db.sequelize.query(`DELETE FROM request_participants WHERE "requestId" = '${testId}'`); + console.log('✅ Clean up successful!'); + } catch (error: any) { + console.error('❌ Manual insert failed:', error.message); + } finally { + process.exit(0); + } +} + +testInsert(); diff --git a/scripts/update_dealer_codes_table.ts b/scripts/update_dealer_codes_table.ts new file mode 100644 index 0000000..5c6430d --- /dev/null +++ b/scripts/update_dealer_codes_table.ts @@ -0,0 +1,46 @@ +import db from '../src/database/models/index.js'; +const { sequelize } = db; + +async function updateDealerCodesTable() { + console.log('🔄 Checking and updating dealer_codes table schema...'); + + try { + // Add applicationId + await sequelize.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='applicationId') THEN + ALTER TABLE dealer_codes ADD COLUMN "applicationId" UUID REFERENCES applications(id); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='salesCode') THEN + ALTER TABLE dealer_codes ADD COLUMN "salesCode" VARCHAR(255); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='serviceCode') THEN + ALTER TABLE dealer_codes ADD COLUMN "serviceCode" VARCHAR(255); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gmaCode') THEN + ALTER TABLE dealer_codes ADD COLUMN "gmaCode" VARCHAR(255); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gearCode') THEN + ALTER TABLE dealer_codes ADD COLUMN "gearCode" VARCHAR(255); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='sapMasterId') THEN + ALTER TABLE dealer_codes ADD COLUMN "sapMasterId" VARCHAR(255); + END IF; + END $$; + `); + + console.log('✅ dealer_codes table schema updated successfully.'); + process.exit(0); + } catch (error) { + console.error('❌ Error updating dealer_codes table:', error); + process.exit(1); + } +} + +updateDealerCodesTable(); diff --git a/scripts/update_enum.ts b/scripts/update_enum.ts new file mode 100644 index 0000000..47491aa --- /dev/null +++ b/scripts/update_enum.ts @@ -0,0 +1,33 @@ +import { APPLICATION_STATUS } from '../src/common/config/constants.js'; +import db from '../src/database/models/index.js'; + +async function updateEnum() { + try { + console.log('🔄 Syncing all APPLICATION_STATUS values with DB Enum...'); + + const statuses = Object.values(APPLICATION_STATUS); + + for (const status of statuses) { + try { + // Posgres doesn't support IF NOT EXISTS for ADD VALUE in 9.5 and below + // so we do it one by one and ignore "already exists" errors. + await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE '${status}'`); + console.log(`✅ Added: ${status}`); + } catch (e: any) { + if (e.message.includes('already exists')) { + // console.log(`ℹ️ Already exists: ${status}`); + } else { + console.error(`❌ Error adding ${status}:`, e.message); + } + } + } + + console.log('\n✅ Database Enum successfully synchronized with constants.'); + } catch (error: any) { + console.error('❌ Critical failure during sync:', error.message); + } finally { + process.exit(0); + } +} + +updateEnum(); diff --git a/scripts/update_participant_enum.ts b/scripts/update_participant_enum.ts new file mode 100644 index 0000000..581a92c --- /dev/null +++ b/scripts/update_participant_enum.ts @@ -0,0 +1,26 @@ +import db from '../src/database/models/index.js'; + +async function updateParticipantEnum() { + try { + console.log('🔄 Adding "architecture" to participantType enum...'); + + try { + await db.sequelize.query(`ALTER TYPE "enum_request_participants_participantType" ADD VALUE 'architecture'`); + console.log(`✅ Added: architecture`); + } catch (e: any) { + if (e.message.includes('already exists')) { + console.log(`ℹ️ Already exists: architecture`); + } else { + console.error(`❌ Error adding architecture:`, e.message); + } + } + + console.log('\n✅ Database Enum successfully updated.'); + } catch (error: any) { + console.error('❌ Critical failure:', error.message); + } finally { + process.exit(0); + } +} + +updateParticipantEnum(); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 3a66b4b..a0ebf8c 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -63,6 +63,7 @@ export const APPLICATION_STATUS = { LEVEL_3_APPROVED: 'Level 3 Approved', FDD_VERIFICATION: 'FDD Verification', PAYMENT_PENDING: 'Payment Pending', + LOI_IN_PROGRESS: 'LOI In Progress', LOI_ISSUED: 'LOI Issued', DEALER_CODE_GENERATION: 'Dealer Code Generation', ARCHITECTURE_TEAM_ASSIGNED: 'Architecture Team Assigned', @@ -304,6 +305,18 @@ export const DOCUMENT_TYPES = { BOARD_RESOLUTION: 'Board Resolution', PROPERTY_DOCUMENTS: 'Property Documents', BANK_STATEMENT: 'Bank Statement', + NODAL_AGREEMENT: 'Nodal Agreement', + CANCELLED_CHECK: 'Cancelled Check', + FIRM_REGISTRATION: 'Firm Registration', + RENTAL_AGREEMENT: 'Rental Agreement', + VIRTUAL_CODE: 'Virtual Code Confirmation', + DOMAIN_ID: 'Domain ID Setup', + MSD_CONFIG: 'MSD Configuration', + LOI_ACK: 'LOI Acknowledgement', + ARCHITECTURE_ASSIGNMENT: 'Architecture Assignment Document', + ARCHITECTURE_BLUEPRINT: 'Architecture Blueprint', + SITE_PLAN: 'Site Plan', + ARCHITECTURE_COMPLETION: 'Architecture Completion Certificate', OTHER: 'Other' } as const; diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index 35966e4..b17a8f5 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -37,6 +37,9 @@ export interface ApplicationAttributes { zoneId: string | null; regionId: string | null; areaId: string | null; + architectureAssignedDate: Date | null; + architectureDocumentDate: Date | null; + architectureCompletionDate: Date | null; score: number; documents: any[]; timeline: any[]; @@ -223,6 +226,18 @@ export default (sequelize: Sequelize) => { key: 'id' } }, + architectureAssignedDate: { + type: DataTypes.DATE, + allowNull: true + }, + architectureDocumentDate: { + type: DataTypes.DATE, + allowNull: true + }, + architectureCompletionDate: { + type: DataTypes.DATE, + allowNull: true + }, documents: { type: DataTypes.JSON, defaultValue: [] @@ -273,6 +288,7 @@ export default (sequelize: Sequelize) => { as: 'participants', scope: { requestType: 'application' } }); + Application.hasOne(models.DealerCode, { foreignKey: 'applicationId', as: 'dealerCode' }); }; return Application; diff --git a/src/database/models/DealerCode.ts b/src/database/models/DealerCode.ts index 6deab7c..49be7fc 100644 --- a/src/database/models/DealerCode.ts +++ b/src/database/models/DealerCode.ts @@ -3,6 +3,12 @@ import { Model, DataTypes, Sequelize } from 'sequelize'; export interface DealerCodeAttribute { id: string; dealerCode: string; + applicationId: string | null; + salesCode: string | null; + serviceCode: string | null; + gmaCode: string | null; + gearCode: string | null; + sapMasterId: string | null; status: string; generatedAt: Date; generatedBy: string | null; @@ -22,6 +28,34 @@ export default (sequelize: Sequelize) => { unique: true, allowNull: false }, + applicationId: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'applications', + key: 'id' + } + }, + salesCode: { + type: DataTypes.STRING, + allowNull: true + }, + serviceCode: { + type: DataTypes.STRING, + allowNull: true + }, + gmaCode: { + type: DataTypes.STRING, + allowNull: true + }, + gearCode: { + type: DataTypes.STRING, + allowNull: true + }, + sapMasterId: { + type: DataTypes.STRING, + allowNull: true + }, status: { type: DataTypes.STRING, defaultValue: 'active' @@ -47,6 +81,7 @@ export default (sequelize: Sequelize) => { (DealerCode as any).associate = (models: any) => { DealerCode.belongsTo(models.User, { foreignKey: 'generatedBy', as: 'generator' }); DealerCode.hasOne(models.Dealer, { foreignKey: 'dealerCodeId', as: 'dealer' }); + DealerCode.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' }); }; return DealerCode; diff --git a/src/database/models/RequestParticipant.ts b/src/database/models/RequestParticipant.ts index 0561d04..f4d7d97 100644 --- a/src/database/models/RequestParticipant.ts +++ b/src/database/models/RequestParticipant.ts @@ -36,7 +36,7 @@ export default (sequelize: Sequelize) => { } }, participantType: { - type: DataTypes.ENUM('owner', 'assignee', 'reviewer', 'contributor', 'observer'), + type: DataTypes.ENUM('owner', 'assignee', 'reviewer', 'contributor', 'observer', 'architecture'), defaultValue: 'contributor' }, joinedMethod: { diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index e5ebfc3..0d072e6 100644 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -163,15 +163,26 @@ export const createUser = async (req: AuthRequest, res: Response) => { }); } - // Check if user already exists - const existingUser = await User.findOne({ where: { email } }); - if (existingUser) { + // Check if user already exists (Email) + const existingEmail = await User.findOne({ where: { email } }); + if (existingEmail) { return res.status(400).json({ success: false, message: 'User with this email already exists' }); } + // Check if user already exists (Employee ID) + if (employeeId) { + const existingEmpId = await User.findOne({ where: { employeeId } }); + if (existingEmpId) { + return res.status(400).json({ + success: false, + message: `User with Employee ID ${employeeId} already exists` + }); + } + } + // Hash default password const hashedPassword = await bcrypt.hash('Admin@123', 10); @@ -203,8 +214,16 @@ export const createUser = async (req: AuthRequest, res: Response) => { }); res.status(201).json({ success: true, message: 'User created successfully', data: user }); - } catch (error) { + } catch (error: any) { console.error('Create user error:', error); + if (error.name === 'SequelizeUniqueConstraintError') { + const field = error.errors[0]?.path || 'field'; + const value = error.errors[0]?.value || ''; + return res.status(400).json({ + success: false, + message: `${field} "${value}" already exists. Please use a unique value.` + }); + } res.status(500).json({ success: false, message: 'Error creating user' }); } }; @@ -282,8 +301,16 @@ export const updateUser = async (req: AuthRequest, res: Response) => { }); res.json({ success: true, message: 'User updated successfully', data: user }); - } catch (error) { + } catch (error: any) { console.error('Update user error:', error); + if (error.name === 'SequelizeUniqueConstraintError') { + const field = error.errors[0]?.path || 'field'; + const value = error.errors[0]?.value || ''; + return res.status(400).json({ + success: false, + message: `${field} "${value}" already exists. Please use a unique value.` + }); + } res.status(500).json({ success: false, message: 'Error updating user' }); } }; diff --git a/src/modules/eor/eor.controller.ts b/src/modules/eor/eor.controller.ts index 2ee4299..5698c54 100644 --- a/src/modules/eor/eor.controller.ts +++ b/src/modules/eor/eor.controller.ts @@ -38,18 +38,20 @@ export const createChecklist = async (req: AuthRequest, res: Response) => { }); if (created) { - // Define Default Mandatory Items per SRS + // Define Default Mandatory Items per SRS/Frontend const defaultItems = [ - { itemType: 'Architecture', description: 'Brand Signage & Facade as per guidelines' }, - { itemType: 'Architecture', description: 'Interior Fit-out & Furniture' }, - { itemType: 'Sales', description: 'Display Vehicles (All models) available' }, - { itemType: 'Sales', description: 'Test Ride Vehicles registered' }, - { itemType: 'Training', description: 'Sales Staff (DSE) Training Completed' }, - { itemType: 'Training', description: 'Service Technician (Pro-Meck) Training' }, - { itemType: 'IT', description: 'DMS (Dealer Management System) configured' }, - { itemType: 'IT', description: 'High-speed internet & IT Hardware ready' }, - { itemType: 'Service', description: 'Special Tools & Equipment installed' }, - { itemType: 'Finance', description: 'Bank Account mapped in SAP' } + { itemType: 'Sales', description: 'Sales Standards' }, + { itemType: 'Service', description: 'Service & Spares' }, + { itemType: 'IT', description: 'DMS infra' }, + { itemType: 'Training', description: 'Manpower Training' }, + { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' }, + { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' }, + { itemType: 'Finance', description: 'Inventory Funding' }, + { itemType: 'IT', description: 'Virtual code availability' }, + { itemType: 'Finance', description: 'Vendor payments' }, + { itemType: 'Marketing', description: 'Details for website submission' }, + { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' }, + { itemType: 'IT', description: 'Auto ordering' } ]; const itemsData = defaultItems.map(item => ({ @@ -75,15 +77,23 @@ export const updateItem = async (req: AuthRequest, res: Response) => { const { checklistId } = req.params; const { itemType, description, isCompliant, remarks, proofDocumentId } = req.body; - const item = await EorChecklistItem.create({ - checklistId, - itemType, - description, - isCompliant, - remarks, - proofDocumentId + let item = await EorChecklistItem.findOne({ + where: { checklistId, description } }); + if (item) { + await item.update({ isCompliant, remarks, proofDocumentId, itemType }); + } else { + item = await EorChecklistItem.create({ + checklistId, + itemType, + description, + isCompliant, + remarks, + proofDocumentId + }); + } + res.status(201).json({ success: true, message: 'Item added/updated', data: item }); } catch (error) { console.error('Update EOR item error:', error); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index a547b8f..7383f20 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -194,7 +194,8 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => { model: db.RequestParticipant, as: 'participants', include: [{ model: db.User, as: 'user', attributes: ['id', ['fullName', 'name'], 'email', ['roleCode', 'role']] }] - } + }, + { model: db.DealerCode, as: 'dealerCode' } ] }); @@ -297,6 +298,54 @@ export const uploadDocuments = async (req: any, res: Response) => { status: 'active' }); + // Update architecture document date if relevant + if (['Architecture Blueprint', 'Site Plan'].includes(documentType)) { + await application.update({ architectureDocumentDate: new Date() }); + } + + // Handle EOR Checklist Automatic Linking & Compliance + const eorItems = [ + { itemType: 'Sales', description: 'Sales Standards' }, + { itemType: 'Service', description: 'Service & Spares' }, + { itemType: 'IT', description: 'DMS infra' }, + { itemType: 'Training', description: 'Manpower Training' }, + { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' }, + { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' }, + { itemType: 'Finance', description: 'Inventory Funding' }, + { itemType: 'IT', description: 'Virtual code availability' }, + { itemType: 'Finance', description: 'Vendor payments' }, + { itemType: 'Marketing', description: 'Details for website submission' }, + { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' }, + { itemType: 'IT', description: 'Auto ordering' } + ]; + + const eorDescriptions = eorItems.map(i => i.description); + + if (eorDescriptions.includes(documentType)) { + // Find or Create Checklist + const [checklist, created] = await db.EorChecklist.findOrCreate({ + where: { applicationId: application.id }, + defaults: { status: 'In Progress' } + }); + + // If newly created or no items exist, seed them + const existingItemCount = await db.EorChecklistItem.count({ where: { checklistId: checklist.id } }); + if (created || existingItemCount === 0) { + const itemsData = eorItems.map(item => ({ + ...item, + checklistId: checklist.id, + isCompliant: false + })); + await db.EorChecklistItem.bulkCreate(itemsData); + } + + // Update the matching item - Link only, don't auto-verify (requested by user) + await db.EorChecklistItem.update( + { proofDocumentId: newDoc.id, isCompliant: false }, + { where: { checklistId: checklist.id, description: documentType } } + ); + } + res.status(201).json({ success: true, message: 'Document uploaded successfully', @@ -424,14 +473,21 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => { export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const { userId, remarks } = req.body; + const { userId, assignedTo, remarks } = req.body; + const targetUserId = userId || assignedTo; + + if (!targetUserId) { + return res.status(400).json({ success: false, message: 'Architecture team member (userId) is required' }); + } const application = await Application.findByPk(id); if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); await application.update({ - architectureAssignedTo: userId, - architectureStatus: 'Assigned', + architectureAssignedTo: targetUserId, + architectureStatus: 'IN_PROGRESS', + architectureAssignedDate: new Date(), + overallStatus: 'Architecture Team Assigned', updatedAt: new Date() }); @@ -440,7 +496,7 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => where: { requestId: application.id, requestType: 'application', - userId, + userId: targetUserId, participantType: 'architecture' }, defaults: { joinedMethod: 'auto' } @@ -451,7 +507,7 @@ export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => action: AUDIT_ACTIONS.UPDATED, entityType: 'application', entityId: application.id, - newData: { architectureAssignedTo: userId, remarks } + newData: { architectureAssignedTo: targetUserId, remarks } }); res.json({ success: true, message: 'Architecture team assigned successfully' }); @@ -469,10 +525,20 @@ export const updateArchitectureStatus = async (req: AuthRequest, res: Response) const application = await Application.findByPk(id); if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); - await application.update({ + const updateData: any = { architectureStatus: status, updatedAt: new Date() - }); + }; + + // Sync overall status if architecture is completed + if (status === 'COMPLETED') { + updateData.overallStatus = 'Architecture Team Completion'; + updateData.architectureCompletionDate = new Date(); + } else if (status === 'IN_PROGRESS') { + updateData.overallStatus = 'Architecture Team Assigned'; + } + + await application.update(updateData); await AuditLog.create({ userId: req.user?.id, @@ -503,20 +569,44 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => { // Save Dealer Codes await db.DealerCode.create({ + dealerCode: sapData.salesCode, // Use sales code as primary dealer code applicationId: id, salesCode: sapData.salesCode, serviceCode: sapData.serviceCode, gmaCode: sapData.gmaCode, gearCode: sapData.gearCode, sapMasterId: sapData.sapMasterId, - status: 'Active' + status: 'Active', + generatedBy: req.user?.id }); + const previousStatus = application.overallStatus; + + // Update application status to reflect codes are generated + // We STAY in Dealer Code Generation until architecture is assigned await application.update({ - overallStatus: 'Architecture Team Assigned', + overallStatus: 'Dealer Code Generation', progressPercentage: 80 }); + // Log Status History + await db.ApplicationStatusHistory.create({ + applicationId: application.id, + previousStatus, + newStatus: 'Dealer Code Generation', + changedBy: req.user?.id, + reason: 'SAP Dealer Codes Generated' + }); + + // Audit Log + await db.AuditLog.create({ + userId: req.user?.id, + action: AUDIT_ACTIONS.DEALER_CODE_GENERATED, + entityType: 'application', + entityId: application.id, + newData: { dealerCode: sapData.salesCode } + }); + res.json({ success: true, message: 'SAP Dealer Codes generated successfully (Mock)',