255 lines
11 KiB
TypeScript
255 lines
11 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import { Op } from 'sequelize';
|
|
import db from '../../database/models/index.js';
|
|
const { FddAssignment, FddReport, Application } = db;
|
|
import { AuthRequest } from '../../types/express.types.js';
|
|
import { AUDIT_ACTIONS, APPLICATION_STATUS, APPLICATION_STAGES, ROLES } from '../../common/config/constants.js';
|
|
import { WorkflowService } from '../../services/WorkflowService.js';
|
|
import { pickApplicationAuditContext, safeAuditLogCreate } from '../../services/applicationAuditLog.service.js';
|
|
import { NotificationService } from '../../services/NotificationService.js';
|
|
|
|
export const getAssignment = async (req: Request, res: Response) => {
|
|
try {
|
|
const { applicationId } = req.params;
|
|
const targetId = applicationId as string;
|
|
|
|
// Resolve application first to get UUID
|
|
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(targetId);
|
|
const application = await Application.findOne({
|
|
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
|
|
});
|
|
|
|
if (!application) {
|
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
|
}
|
|
|
|
const assignment = await FddAssignment.findOne({
|
|
where: { applicationId: application.id },
|
|
include: [{
|
|
model: FddReport,
|
|
as: 'reports',
|
|
include: [
|
|
{ model: db.User, as: 'submitter', attributes: ['fullName'] },
|
|
{ model: db.User, as: 'verifier', attributes: ['fullName'] }
|
|
]
|
|
}]
|
|
});
|
|
res.json({ success: true, data: assignment });
|
|
} catch (error) {
|
|
console.error('Get FDD assignment error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching FDD assignment' });
|
|
}
|
|
};
|
|
|
|
export const assignAgency = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { applicationId, assignedToAgency } = req.body;
|
|
const targetId = applicationId as string;
|
|
|
|
// Resolve application first to get UUID
|
|
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(targetId);
|
|
const application = await Application.findOne({
|
|
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
|
|
});
|
|
|
|
if (!application) {
|
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
|
}
|
|
|
|
const assignment = await FddAssignment.create({
|
|
applicationId: application.id,
|
|
assignedToAgency, // Agency User ID
|
|
status: 'Assigned'
|
|
});
|
|
|
|
// Bridge: Transition application to active FDD stage
|
|
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.FDD_VERIFICATION, req.user?.id || null, {
|
|
reason: 'FDD Agency assigned. Initiating financial due diligence.',
|
|
stage: APPLICATION_STAGES.FDD,
|
|
progressPercentage: 70
|
|
});
|
|
|
|
await safeAuditLogCreate({
|
|
userId: req.user?.id,
|
|
action: AUDIT_ACTIONS.FDD_ASSIGNED,
|
|
entityType: 'fdd_assignment',
|
|
entityId: assignment.id,
|
|
newData: { assignedToAgency, applicationId: application.id },
|
|
});
|
|
await safeAuditLogCreate({
|
|
userId: req.user?.id,
|
|
action: AUDIT_ACTIONS.FDD_ASSIGNED,
|
|
entityType: 'application',
|
|
entityId: application.id,
|
|
newData: {
|
|
assignmentId: assignment.id,
|
|
assignedToAgency,
|
|
note: 'FDD agency assigned; application moved to FDD verification.',
|
|
context: pickApplicationAuditContext(application),
|
|
},
|
|
});
|
|
|
|
// SRS §6.15.3.1 — Notify assigned FDD agency user of their assignment
|
|
const fddUser = await db.User.findByPk(assignedToAgency);
|
|
if (fddUser) {
|
|
const phone = (fddUser as any).mobileNumber || null;
|
|
NotificationService.notify(fddUser.id, fddUser.email, {
|
|
title: `FDD Assignment: ${application.applicationId}`,
|
|
message: `You have been assigned to conduct Financial Due Diligence for application ${application.applicationId}.`,
|
|
channels: phone ? ['system', 'email', 'whatsapp'] : ['system', 'email'],
|
|
templateCode: 'USER_ASSIGNED',
|
|
placeholders: {
|
|
applicantName: application.applicantName || '',
|
|
applicationId: application.applicationId,
|
|
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
|
|
ctaLabel: 'View Assignment',
|
|
phone: phone || ''
|
|
}
|
|
}).catch((e: any) => console.error('[FDD] Agency notify failed:', e));
|
|
}
|
|
|
|
res.status(201).json({ success: true, message: 'FDD Agency assigned', data: assignment });
|
|
} catch (error) {
|
|
console.error('Assign FDD agency error:', error);
|
|
res.status(500).json({ success: false, message: 'Error assigning agency' });
|
|
}
|
|
};
|
|
|
|
export const uploadReport = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { assignmentId, reportDocumentId, findings, recommendation, applicationId } = req.body;
|
|
let finalAssignmentId = assignmentId;
|
|
|
|
// Auto-assign logic if assignmentId is missing
|
|
if (!finalAssignmentId && applicationId) {
|
|
const appId = applicationId;
|
|
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(appId);
|
|
const application = await Application.findOne({
|
|
where: isUUID ? { [Op.or]: [{ id: appId }, { applicationId: appId }] } : { applicationId: appId }
|
|
});
|
|
|
|
if (application) {
|
|
const [newAssignment] = await FddAssignment.findOrCreate({
|
|
where: { applicationId: application.id },
|
|
defaults: {
|
|
assignedToAgency: req.user?.id,
|
|
status: 'Assigned'
|
|
}
|
|
});
|
|
finalAssignmentId = newAssignment.id;
|
|
}
|
|
}
|
|
|
|
if (!finalAssignmentId) {
|
|
return res.status(400).json({ success: false, message: 'Assignment ID or valid Application ID is required' });
|
|
}
|
|
|
|
let report = await FddReport.findOne({ where: { assignmentId: finalAssignmentId } });
|
|
|
|
if (report) {
|
|
await report.update({
|
|
reportDocumentId,
|
|
findings,
|
|
recommendation,
|
|
submittedBy: req.user?.id
|
|
});
|
|
} else {
|
|
report = await FddReport.create({
|
|
assignmentId: finalAssignmentId,
|
|
reportDocumentId,
|
|
findings,
|
|
recommendation,
|
|
submittedBy: req.user?.id
|
|
});
|
|
}
|
|
|
|
// Update Assignment Status
|
|
await FddAssignment.update(
|
|
{ status: 'Report Submitted' },
|
|
{ where: { id: finalAssignmentId } }
|
|
);
|
|
|
|
// Fetch application to transition
|
|
const assignment = await FddAssignment.findByPk(finalAssignmentId);
|
|
if (assignment) {
|
|
const application = await Application.findByPk(assignment.applicationId);
|
|
if (application) {
|
|
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.FDD_VERIFICATION, req.user?.id || null, {
|
|
reason: 'FDD Report uploaded. Pending review to proceed to LOI stage.',
|
|
forceLog: true // Log even if status is same
|
|
});
|
|
|
|
// SRS §6.15.3.1 — Notify Finance + DD-Admin that FDD report is submitted
|
|
const fddReportRoles = [ROLES.FINANCE, ROLES.DD_ADMIN];
|
|
for (const role of fddReportRoles) {
|
|
const roleUsers = await db.User.findAll({ where: { roleCode: role } });
|
|
for (const u of roleUsers) {
|
|
NotificationService.notify(u.id, u.email, {
|
|
title: `FDD Report Submitted: ${application.applicationId}`,
|
|
message: `The FDD agency has submitted their financial due diligence report for ${application.applicationId}. Review is required before LOI stage.`,
|
|
channels: ['system', 'email'],
|
|
templateCode: 'WORKFLOW_ACTION_REQUIRED',
|
|
placeholders: {
|
|
dealerName: application.applicantName || '',
|
|
requestId: application.applicationId,
|
|
targetStage: 'FDD Review',
|
|
link: `${process.env.FRONTEND_URL || 'http://localhost:5173'}/applications/${application.id}`,
|
|
phone: ''
|
|
}
|
|
}).catch((e: any) => console.error('[FDD] Finance/Admin notify failed:', e));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
res.status(201).json({ success: true, message: 'FDD Report uploaded successfully. Pending Admin review.', data: report });
|
|
} catch (error) {
|
|
console.error('Upload FDD report error:', error);
|
|
res.status(500).json({ success: false, message: 'Error uploading report' });
|
|
}
|
|
};
|
|
|
|
export const flagNonResponsive = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { applicationId, remarks } = req.body;
|
|
const targetId = applicationId as string;
|
|
|
|
// Resolve application first to get UUID
|
|
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(targetId);
|
|
const application = await Application.findOne({
|
|
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
|
|
});
|
|
|
|
if (!application) {
|
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
|
}
|
|
|
|
const previousStatutory = application.statutoryStatus;
|
|
// 1. Update Application status at model level
|
|
await application.update({ statutoryStatus: 'Flagged' });
|
|
|
|
console.log(`[FDDController] Flagging application ${application.id} as non-responsive (FDD)`);
|
|
await safeAuditLogCreate({
|
|
userId: req.user?.id,
|
|
action: AUDIT_ACTIONS.FDD_FLAGGED_NON_RESPONSIVE,
|
|
entityType: 'application',
|
|
entityId: application.id,
|
|
oldData: {
|
|
statutoryStatus: previousStatutory,
|
|
overallStatus: application.overallStatus,
|
|
currentStage: application.currentStage,
|
|
},
|
|
newData: {
|
|
statutoryStatus: 'Flagged',
|
|
remarks: remarks || 'Applicant is non-responsive to FDD queries.',
|
|
context: pickApplicationAuditContext(application),
|
|
},
|
|
});
|
|
|
|
res.json({ success: true, message: 'Application flagged successfully' });
|
|
} catch (error) {
|
|
console.error('Flag non-responsive error:', error);
|
|
res.status(500).json({ success: false, message: 'Error highlighting non-responsiveness' });
|
|
}
|
|
};
|