396 lines
18 KiB
TypeScript
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' });
|
|
}
|
|
};
|