Dealer_Onboarding_Backend/src/modules/eor/eor.controller.ts

396 lines
18 KiB
TypeScript

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<string, string> = {
'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<void> {
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' });
}
};