import { Request, Response } from 'express'; import { Op } from 'sequelize'; import db from '../../database/models/index.js'; const { EorChecklist, EorChecklistItem, OnboardingDocument, RelocationDocument } = db; import { AuthRequest } from '../../types/express.types.js'; import { NotificationService } from '../../services/NotificationService.js'; import { ROLES } from '../../common/config/constants.js'; /** Default EOR rows for relocation (SRS 12.2.8) — must stay aligned with relocation required-doc labels. */ export const RELOCATION_EOR_DEFAULT_ITEMS = [ { itemType: 'Property', description: 'Property documents for new location' }, { itemType: 'Property', description: 'Lease / Rental agreement' }, { itemType: 'Property', description: 'Layout / Floor plan of new location' }, { itemType: 'Infrastructure', description: 'Photos of new location' }, { itemType: 'Infrastructure', description: 'Locality map / Building plan approval' }, { itemType: 'Statutory', description: 'NOC from current landlord' }, { itemType: 'Statutory', description: 'Municipal approvals (Fire safety / Pollution clearance)' }, { itemType: 'Utility', description: 'Electricity & Water supply documents' } ] as const; /** * Maps `RelocationDocument.documentType` (same strings as relocation required-doc UI) to EOR checklist row text. */ const RELOCATION_UPLOAD_TYPE_TO_EOR_DESCRIPTION: Record = { 'Property documents for new location': 'Property documents for new location', 'Lease/Rental agreement for new location': 'Lease / Rental agreement', 'NOC from current landlord': 'NOC from current landlord', 'Municipal approvals': 'Municipal approvals (Fire safety / Pollution clearance)', 'Fire safety certificate': 'Municipal approvals (Fire safety / Pollution clearance)', 'Pollution clearance': 'Municipal approvals (Fire safety / Pollution clearance)', 'Layout/Floor plan of new location': 'Layout / Floor plan of new location', 'Photos of new location': 'Photos of new location', 'Locality map': 'Locality map / Building plan approval', 'Building plan approval': 'Locality map / Building plan approval', 'Electricity connection documents': 'Electricity & Water supply documents', 'Water supply documents': 'Electricity & Water supply documents' }; async function mapRelocationDocumentsToEorItems(checklistId: string, relocationId: string) { const docs = await RelocationDocument.findAll({ where: { relocationId, status: { [Op.notIn]: ['Rejected'] } } }); for (const doc of docs) { const dt = String((doc as any).documentType || '').trim(); const eorDesc = RELOCATION_UPLOAD_TYPE_TO_EOR_DESCRIPTION[dt]; if (!eorDesc) continue; const verified = String((doc as any).status || '').toLowerCase() === 'verified'; let item = await EorChecklistItem.findOne({ where: { checklistId, description: { [Op.iLike]: eorDesc }, proofDocumentId: { [Op.is]: null } } }); if (!item) { const linked = await EorChecklistItem.findOne({ where: { checklistId, description: { [Op.iLike]: eorDesc }, proofDocumentId: (doc as any).id } }); if (linked && verified) { await linked.update({ isCompliant: true }); } continue; } await item.update({ proofDocumentId: (doc as any).id, isCompliant: verified ? true : item.isCompliant }); } } /** * Relocation flow historically called `EorChecklist.findOrCreate` only, so checklists existed with **zero items** * and no link to `relocation_documents`. Heal by seeding default rows and attaching proofs by document type. */ export async function ensureRelocationEorChecklistSeeded(relocationId: string): Promise { const checklist = await EorChecklist.findOne({ where: { relocationId } }); if (!checklist) return; const itemCount = await EorChecklistItem.count({ where: { checklistId: checklist.id } }); if (itemCount === 0) { const compliantDefault = String(checklist.status || '').toLowerCase() === 'completed'; await EorChecklistItem.bulkCreate( RELOCATION_EOR_DEFAULT_ITEMS.map((item) => ({ itemType: item.itemType, description: item.description, checklistId: checklist.id, isCompliant: compliantDefault })) ); } await mapRelocationDocumentsToEorItems(checklist.id, relocationId); } export const getChecklist = async (req: Request, res: Response) => { try { const { applicationId, relocationId } = req.params; // Resolve human-readable applicationId (e.g. APP-2026-79CE90) to UUID let resolvedAppId = applicationId as string; if (applicationId) { const appIdStr = applicationId as string; 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(appIdStr); if (!isUUID) { const app = await db.Application.findOne({ where: { applicationId: appIdStr } }); if (!app) { res.status(404).json({ success: false, message: 'Application not found' }); return; } resolvedAppId = app.id; } } // Resolve relocation route param (UUID or REL-2026-xxxx) to UUID for DB lookup let resolvedRelocationId: string | undefined; if (relocationId) { const relStr = relocationId as string; const isRelUUID = /^[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(relStr); if (isRelUUID) { resolvedRelocationId = relStr; } else { const rel = await db.RelocationRequest.findOne({ where: { requestId: relStr } }); if (!rel) { res.status(404).json({ success: false, message: 'Relocation request not found' }); return; } resolvedRelocationId = rel.id; } } let checklist = await EorChecklist.findOne({ where: resolvedRelocationId ? { relocationId: resolvedRelocationId } : { applicationId: resolvedAppId }, include: [{ model: EorChecklistItem, as: 'items' }] }); if (!checklist) { res.status(404).json({ success: false, message: 'Checklist not found' }); return; } if (resolvedRelocationId) { await ensureRelocationEorChecklistSeeded(resolvedRelocationId); checklist = await EorChecklist.findOne({ where: { relocationId: resolvedRelocationId }, include: [{ model: EorChecklistItem, as: 'items' }] }); if (!checklist) { res.status(404).json({ success: false, message: 'Checklist not found' }); return; } } const items = checklist.items || []; const proofDocIds = items.map((i: any) => i.proofDocumentId).filter(Boolean); let payload: any = checklist.toJSON ? checklist.toJSON() : checklist; if (proofDocIds.length > 0) { let docs = []; if (resolvedRelocationId) { docs = await RelocationDocument.findAll({ where: { id: proofDocIds } }); } else { docs = await OnboardingDocument.findAll({ where: { id: proofDocIds } }); } const docsMap = new Map(docs.map((d: any) => [d.id, d])); payload = { ...payload }; payload.items = (payload.items || []).map((item: any) => ({ ...item, proofDocument: docsMap.get(item.proofDocumentId) || null })); } res.json({ success: true, data: payload }); } catch (error) { console.error('Get EOR checklist error:', error); res.status(500).json({ success: false, message: 'Error fetching EOR checklist' }); } }; export const createChecklist = async (req: AuthRequest, res: Response) => { try { const { applicationId: rawAppId, relocationId } = req.body; if (!rawAppId && !relocationId) { return res.status(400).json({ success: false, message: 'applicationId or relocationId is required' }); } // Resolve applicationId to UUID (handles readable IDs like APP-2026-79CE90) let resolvedAppId: string | null = null; if (rawAppId) { 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(rawAppId); const application = isUUID ? await db.Application.findByPk(rawAppId) : await db.Application.findOne({ where: { applicationId: rawAppId } }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); resolvedAppId = application.id; } else if (relocationId) { const relocation = await db.RelocationRequest.findByPk(relocationId); if (!relocation) return res.status(404).json({ success: false, message: 'Relocation request not found' }); } const [checklist, created] = await EorChecklist.findOrCreate({ where: relocationId ? { relocationId } : { applicationId: resolvedAppId }, defaults: { status: 'In Progress', applicationId: resolvedAppId || null, relocationId: relocationId || null } }); if (created) { // Define Default Mandatory Items per SRS/Frontend let defaultItems: { itemType: string; description: string }[] = []; if (relocationId) { defaultItems = [...RELOCATION_EOR_DEFAULT_ITEMS]; } else { // Onboarding Default defaultItems = [ { 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 => ({ ...item, checklistId: checklist.id, isCompliant: false })); await EorChecklistItem.bulkCreate(itemsData); // AUTO-MAP existing documents from OnboardingDocument table if (resolvedAppId) { const existingDocs = await OnboardingDocument.findAll({ where: { applicationId: resolvedAppId, status: 'active' } }); if (existingDocs.length > 0) { const typeMap: any = { 'GST Certificate': 'GST certificate including Accessories & Apparels billing', 'Virtual Code Confirmation': 'Virtual code availability', 'Trade Certificate': 'Trade certificate with test ride bikes registration', 'DMS Infra Details': 'DMS infra' }; for (const doc of existingDocs) { const targetDescription = typeMap[doc.documentType] || doc.documentType; await EorChecklistItem.update( { proofDocumentId: doc.id }, { where: { checklistId: checklist.id, description: { [Op.iLike]: targetDescription.trim() } } } ); } } } else if (relocationId) { await mapRelocationDocumentsToEorItems(checklist.id, relocationId); } } // Status transition will be handled by the global handleApprove workflow or explicit trigger // await application.update({ overallStatus: 'EOR In Progress' }); res.status(201).json({ success: true, message: 'EOR Checklist initiated with default items', data: checklist }); } catch (error) { console.error('Create EOR checklist error:', error); res.status(500).json({ success: false, message: 'Error creating checklist' }); } } export const updateItem = async (req: AuthRequest, res: Response) => { try { const { checklistId } = req.params; const { itemType, description, isCompliant, remarks, proofDocumentId } = req.body; 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); res.status(500).json({ success: false, message: 'Error updating item' }); } }; export const submitAudit = async (req: AuthRequest, res: Response) => { try { const { checklistId } = req.params; // or ID const { status, overallComments } = req.body; await EorChecklist.update( { status, overallComments, auditDate: new Date(), auditorId: req.user?.id }, { where: { id: checklistId } } ); if (status === 'Completed') { const checklist = await EorChecklist.findByPk(checklistId); if (checklist) { if (checklist.applicationId) { await db.Application.update({ overallStatus: 'Inauguration', progressPercentage: 100 }, { where: { id: checklist.applicationId } }); // Update Progress Tracking const { updateApplicationProgress } = await import('../../common/utils/progress.js'); await updateApplicationProgress(checklist.applicationId, 'EOR Complete', 'completed', 100); await updateApplicationProgress(checklist.applicationId, 'Inauguration', 'active', 50); // SRS §6.19.3.4 — Readiness alert to DD-Head and NBH on EOR 100% completion const app = await db.Application.findByPk(checklist.applicationId); const eorAlertRoles = [ROLES.DD_HEAD, ROLES.NBH, ROLES.DD_ADMIN]; for (const role of eorAlertRoles) { const roleUsers = await db.User.findAll({ where: { roleCode: role } }); for (const u of roleUsers) { NotificationService.notify(u.id, u.email, { title: `EOR Completed: ${app?.applicationId || checklist.applicationId}`, message: `EOR checklist is 100% complete for ${app?.applicantName || 'the applicant'}. Dealership is ready for inauguration.`, channels: ['system', 'email'], templateCode: 'EOR_COMPLETED', placeholders: { applicantName: app?.applicantName || '', applicationId: app?.applicationId || '', link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${checklist.applicationId}`, ctaLabel: 'View Application' } }).catch((e: any) => console.error('[EOR] Completion notify failed:', e)); } } } else if (checklist.relocationId) { await db.RelocationRequest.update({ status: 'Completed', progressPercentage: 100, currentStage: 'Completed' }, { where: { id: checklist.relocationId } }); // SRS §6.19.3.4 — Relocation EOR complete — notify DD-Admin const adminUsers = await db.User.findAll({ where: { roleCode: ROLES.DD_ADMIN } }); for (const u of adminUsers) { NotificationService.notify(u.id, u.email, { title: `Relocation EOR Completed`, message: `The EOR checklist for relocation ${checklist.relocationId} has been fully verified.`, channels: ['system', 'email'], templateCode: 'EOR_COMPLETED', placeholders: { applicantName: '', applicationId: checklist.relocationId, link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/relocation-requests/${checklist.relocationId}`, ctaLabel: 'View Request' } }).catch((e: any) => console.error('[EOR] Relocation notify failed:', e)); } } } } res.json({ success: true, message: 'EOR Audit submitted' }); } catch (error) { res.status(500).json({ success: false, message: 'Error submitting audit' }); } };