auditlogs enhanced end to end flow checked for the onboarding , cursor used for major file chnages

This commit is contained in:
laxman h 2026-04-14 20:12:30 +05:30
parent 1d885f9d9f
commit 7e1e43bef3
34 changed files with 1348 additions and 268 deletions

19
check_loa.js Normal file
View File

@ -0,0 +1,19 @@
import db from './src/database/models/index.js';
async function check() {
try {
const progress = await db.ApplicationProgress.findAll({
where: { applicationId: 'a8d0ffb3-be90-4aa8-9344-16729ed07056' },
order: [['stageOrder', 'ASC']]
});
for (const p of progress) {
console.log(`${p.stageOrder}: ${p.stageName} - Status: ${p.status}`);
}
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
}
check();

27
check_stuck.js Normal file
View File

@ -0,0 +1,27 @@
import db from './src/database/models/index.js';
async function check() {
try {
const applicationId = 'APP-2026-2709';
const application = await db.Application.findOne({ where: { applicationId } });
if (!application) {
console.log('Application not found');
process.exit(0);
}
const request = await db.LoaRequest.findOne({ where: { applicationId: application.id } });
if (request) {
console.log(`Request ID: ${request.id}, Status: ${request.status}`);
} else {
console.log('No LoaRequest found');
}
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
}
check();

30
heal_one.js Normal file
View File

@ -0,0 +1,30 @@
import db from './src/database/models/index.js';
import { WorkflowService } from './src/services/WorkflowService.js';
import { APPLICATION_STATUS } from './src/common/config/constants.js';
async function heal() {
try {
const applicationId = 'a8d0ffb3-be90-4aa8-9344-16729ed07056';
const application = await db.Application.findByPk(applicationId);
console.log(`Healing application ${application.applicationId}...`);
const request = await db.LoaRequest.findOne({ where: { applicationId: application.id } });
if (request) {
await request.update({ status: 'Approved', approvedAt: new Date() });
}
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.EOR_IN_PROGRESS, '18122cf4-d905-484c-a9ff-7b6229f101ef', {
reason: 'Manually recovered: Policy met with 3 approvals.',
progressPercentage: 97
});
console.log('Done.');
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
}
heal();

30
heal_two.js Normal file
View File

@ -0,0 +1,30 @@
import db from './src/database/models/index.js';
import { WorkflowService } from './src/services/WorkflowService.js';
import { APPLICATION_STATUS } from './src/common/config/constants.js';
async function heal() {
try {
const applicationId = 'f2f55d38-befc-4d9a-b216-4e39fb2fb30e';
const application = await db.Application.findByPk(applicationId);
console.log(`Healing application ${application.applicationId}...`);
const request = await db.LoaRequest.findOne({ where: { applicationId: application.id } });
if (request) {
await request.update({ status: 'Approved', approvedAt: new Date() });
}
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.EOR_IN_PROGRESS, '18122cf4-d905-484c-a9ff-7b6229f101ef', {
reason: 'Manually recovered: LOA Policy met with required DD Head & NBH approvals.',
progressPercentage: 97
});
console.log('Done.');
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
}
heal();

View File

@ -0,0 +1,17 @@
import assert from 'node:assert/strict';
import { getResignationStatusForStage, getTerminationStatusForStage, normalizeClearanceStatus, normalizeFnFStatus } from '../src/common/utils/offboardingStatus.js';
assert.equal(normalizeFnFStatus('settled'), 'Completed');
assert.equal(normalizeFnFStatus('finance approval'), 'Finance Approval');
assert.equal(getResignationStatusForStage('ASM'), 'ASM Review');
assert.equal(getResignationStatusForStage('F&F Initiated'), 'F&F Initiated');
assert.equal(getTerminationStatusForStage('Submitted'), 'Submitted');
assert.equal(getTerminationStatusForStage('Terminated'), 'Terminated');
assert.equal(normalizeClearanceStatus('Cleared', 0), 'NOC Submitted');
assert.equal(normalizeClearanceStatus('Cleared', 100), 'Dues Pending');
assert.equal(normalizeClearanceStatus('Pending', 0), 'Pending');
console.log('Offboarding status normalization checks passed.');

View File

@ -319,6 +319,7 @@ export const AUDIT_ACTIONS = {
// FDD // FDD
FDD_ASSIGNED: 'FDD_ASSIGNED', FDD_ASSIGNED: 'FDD_ASSIGNED',
FDD_REPORT_UPLOADED: 'FDD_REPORT_UPLOADED', FDD_REPORT_UPLOADED: 'FDD_REPORT_UPLOADED',
FDD_FLAGGED_NON_RESPONSIVE: 'FDD_FLAGGED_NON_RESPONSIVE',
// LOI & LOA // LOI & LOA
LOI_REQUESTED: 'LOI_REQUESTED', LOI_REQUESTED: 'LOI_REQUESTED',
@ -413,6 +414,53 @@ export const DOCUMENT_TYPES = {
OTHER: 'Other' OTHER: 'Other'
} as const; } as const;
export const RESIGNATION_DOCUMENT_TYPES = [
'Resignation Letter',
'Dealer Undertaking',
'Approval Note',
'Legal Communication',
'Handover Document',
'Settlement Supporting Document',
'Other'
] as const;
export const RESIGNATION_DOCUMENT_STAGES = [
'ASM',
'RBM',
'ZBH',
'DD Lead',
'NBH',
'DD Admin',
'Legal',
'F&F Initiated'
] as const;
export const TERMINATION_DOCUMENT_TYPES = [
'Termination Recommendation',
'Show Cause Notice',
'SCN Response',
'Hearing Record',
'Approval Note',
'Termination Letter',
'Settlement Supporting Document',
'Other'
] as const;
export const TERMINATION_DOCUMENT_STAGES = [
'Submitted',
'RBM Review',
'ZBH Review',
'DD Lead Review',
'Legal Verification',
'NBH Evaluation',
'Show Cause Notice',
'Personal Hearing',
'NBH Final Approval',
'CCO Approval',
'CEO Final Approval',
'Legal - Termination Letter'
] as const;
// Request Types // Request Types
export const REQUEST_TYPES = { export const REQUEST_TYPES = {
APPLICATION: 'application', APPLICATION: 'application',

View File

@ -42,6 +42,7 @@ export const authenticate = async (req: AuthRequest, res: Response, next: NextFu
req.user = { req.user = {
id: application.id, id: application.id,
email: application.email, email: application.email,
phone: application.phone, // Adding phone here
firstName: application.applicantName ? application.applicantName.split(' ')[0] : 'Prospective', firstName: application.applicantName ? application.applicantName.split(' ')[0] : 'Prospective',
lastName: application.applicantName ? application.applicantName.split(' ').slice(1).join(' ') : 'User', lastName: application.applicantName ? application.applicantName.split(' ').slice(1).join(' ') : 'User',
fullName: application.applicantName, fullName: application.applicantName,

View File

@ -0,0 +1,70 @@
import { FNF_STATUS, RESIGNATION_STAGES, TERMINATION_STAGES } from '../config/constants.js';
const FNF_STATUS_ALIASES: Record<string, string> = {
initiated: FNF_STATUS.INITIATED,
new: FNF_STATUS.INITIATED,
draft: FNF_STATUS.INITIATED,
dd_clearance: FNF_STATUS.DD_CLEARANCE,
'dd clearance': FNF_STATUS.DD_CLEARANCE,
legal_clearance: FNF_STATUS.LEGAL_CLEARANCE,
'legal clearance': FNF_STATUS.LEGAL_CLEARANCE,
finance_approval: FNF_STATUS.FINANCE_APPROVAL,
'finance approval': FNF_STATUS.FINANCE_APPROVAL,
completed: FNF_STATUS.COMPLETED,
settled: FNF_STATUS.COMPLETED
};
export const normalizeFnFStatus = (status: string | null | undefined): string => {
if (!status) return FNF_STATUS.INITIATED;
const key = status.trim().toLowerCase();
return FNF_STATUS_ALIASES[key] || status;
};
export const getResignationStatusForStage = (stage: string): string => {
switch (stage) {
case RESIGNATION_STAGES.ASM:
case RESIGNATION_STAGES.RBM:
case RESIGNATION_STAGES.ZBH:
case RESIGNATION_STAGES.DD_LEAD:
case RESIGNATION_STAGES.NBH:
case RESIGNATION_STAGES.DD_ADMIN:
return `${stage} Review`;
case RESIGNATION_STAGES.LEGAL:
return 'Legal - Resignation Letter';
case RESIGNATION_STAGES.FNF_INITIATED:
return RESIGNATION_STAGES.FNF_INITIATED;
case RESIGNATION_STAGES.COMPLETED:
return 'Completed';
case RESIGNATION_STAGES.REJECTED:
return 'Rejected';
default:
return stage;
}
};
export const getTerminationStatusForStage = (stage: string): string => {
switch (stage) {
case TERMINATION_STAGES.SUBMITTED:
return 'Submitted';
case TERMINATION_STAGES.TERMINATED:
return 'Terminated';
case TERMINATION_STAGES.REJECTED:
return 'Rejected';
default:
return stage;
}
};
export const normalizeClearanceStatus = (status: string | null | undefined, amount: number): string => {
const normalizedAmount = Math.abs(Number(amount) || 0);
const value = (status || '').toLowerCase();
if (value === 'cleared' || value === 'noc submitted') {
return normalizedAmount > 0 ? 'Dues Pending' : 'NOC Submitted';
}
if (value === 'dues' || value === 'dues pending') return 'Dues Pending';
if (value === 'n/a') return 'N/A';
if (value === 'pending') return 'Pending';
return normalizedAmount > 0 ? 'Dues Pending' : 'NOC Submitted';
};

View File

@ -143,8 +143,8 @@ export const syncApplicationProgress = async (applicationId: string, overallStat
// Statuses that imply the CURRENT stage (single or both parallel) is finished // Statuses that imply the CURRENT stage (single or both parallel) is finished
const completionStatuses = [ const completionStatuses = [
'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved', 'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved',
'Level 2 Approved', 'Level 3 Approved', 'LOI Issued', 'Level 2 Approved', 'Level 3 Approved',
'LOA Issued', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded' 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
]; ];
const isCurrentStageFinished = completionStatuses.includes(overallStatus); const isCurrentStageFinished = completionStatuses.includes(overallStatus);

View File

@ -38,7 +38,6 @@ import createTerminationAudit from './TerminationAudit.js';
import createFnFAudit from './FnFAudit.js'; import createFnFAudit from './FnFAudit.js';
import createConstitutionalAudit from './ConstitutionalAudit.js'; import createConstitutionalAudit from './ConstitutionalAudit.js';
import createRelocationAudit from './RelocationAudit.js'; import createRelocationAudit from './RelocationAudit.js';
import createDealerBankDetail from './DealerBankDetail.js';
// Batch 1: Organizational Hierarchy & User Management // Batch 1: Organizational Hierarchy & User Management
import createRole from './Role.js'; import createRole from './Role.js';
@ -67,6 +66,7 @@ import createAiSummary from './AiSummary.js';
// Batch 4: Dealer Entity, Documents & Work Notes // Batch 4: Dealer Entity, Documents & Work Notes
import createDealer from './Dealer.js'; import createDealer from './Dealer.js';
import createDealerCode from './DealerCode.js'; import createDealerCode from './DealerCode.js';
import createDealerBankDetail from './DealerBankDetail.js';
import createDocumentVersion from './DocumentVersion.js'; import createDocumentVersion from './DocumentVersion.js';
import createWorkNoteTag from './WorkNoteTag.js'; import createWorkNoteTag from './WorkNoteTag.js';
import createWorkNoteAttachment from './WorkNoteAttachment.js'; import createWorkNoteAttachment from './WorkNoteAttachment.js';
@ -157,7 +157,6 @@ db.TerminationAudit = createTerminationAudit(sequelize);
db.FnFAudit = createFnFAudit(sequelize); db.FnFAudit = createFnFAudit(sequelize);
db.ConstitutionalAudit = createConstitutionalAudit(sequelize); db.ConstitutionalAudit = createConstitutionalAudit(sequelize);
db.RelocationAudit = createRelocationAudit(sequelize); db.RelocationAudit = createRelocationAudit(sequelize);
db.DealerBankDetail = createDealerBankDetail(sequelize);
// Batch 1: Organizational Hierarchy & User Management // Batch 1: Organizational Hierarchy & User Management
db.Role = createRole(sequelize); db.Role = createRole(sequelize);
@ -186,6 +185,7 @@ db.AiSummary = createAiSummary(sequelize);
// Batch 4: Dealer Entity, Documents & Work Notes // Batch 4: Dealer Entity, Documents & Work Notes
db.Dealer = createDealer(sequelize); db.Dealer = createDealer(sequelize);
db.DealerCode = createDealerCode(sequelize); db.DealerCode = createDealerCode(sequelize);
db.DealerBankDetail = createDealerBankDetail(sequelize);
db.DocumentVersion = createDocumentVersion(sequelize); db.DocumentVersion = createDocumentVersion(sequelize);
db.WorkNoteTag = createWorkNoteTag(sequelize); db.WorkNoteTag = createWorkNoteTag(sequelize);
db.WorkNoteAttachment = createWorkNoteAttachment(sequelize); db.WorkNoteAttachment = createWorkNoteAttachment(sequelize);

View File

@ -173,9 +173,19 @@ export const getPermissions = async (req: Request, res: Response) => {
export const getAllUsers = async (req: Request, res: Response) => { export const getAllUsers = async (req: Request, res: Response) => {
try { try {
const { roleCode, locationId } = req.query; const { roleCode, locationId, search, page = 1, limit = 100 } = req.query as any;
const whereClause: any = {}; const whereClause: any = {};
// 1. Search filter
if (search) {
whereClause[Op.or] = [
{ fullName: { [Op.iLike]: `%${search}%` } },
{ email: { [Op.iLike]: `%${search}%` } },
{ employeeId: { [Op.iLike]: `%${search}%` } }
];
}
// 2. Role filter
let rawRoleCode: any = roleCode || req.query['roleCode[]']; let rawRoleCode: any = roleCode || req.query['roleCode[]'];
let finalRoleCodes: string[] = []; let finalRoleCodes: string[] = [];
@ -213,9 +223,11 @@ export const getAllUsers = async (req: Request, res: Response) => {
} }
} }
const users = await User.findAll({ const { count, rows: users } = await User.findAndCountAll({
where: whereClause, where: whereClause,
attributes: { exclude: ['password'] }, attributes: { exclude: ['password'] },
limit: Number(limit),
offset: (Number(page) - 1) * Number(limit),
include: [ include: [
{ {
model: Role, model: Role,
@ -248,7 +260,8 @@ export const getAllUsers = async (req: Request, res: Response) => {
] ]
} }
], ],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']],
distinct: true
}); });
const result = users.map((u: any) => { const result = users.map((u: any) => {
@ -288,7 +301,7 @@ export const getAllUsers = async (req: Request, res: Response) => {
return userJson; return userJson;
}); });
res.json({ success: true, data: result }); res.json({ success: true, data: result, total: count });
} catch (error) { } catch (error) {
console.error('Get users error:', error); console.error('Get users error:', error);
res.status(500).json({ success: false, message: 'Error fetching users' }); res.status(500).json({ success: false, message: 'Error fetching users' });

View File

@ -226,21 +226,6 @@ const processStageDecision = async (params: {
targetStage = 'Statutory Work'; targetStage = 'Statutory Work';
targetProgress = 85; targetProgress = 85;
} else if (stageCode === 'LOA_APPROVAL') { } else if (stageCode === 'LOA_APPROVAL') {
// Hard-stop validation: Check for statutory and bank details
const missingFields = [];
if (!application.panNumber) missingFields.push('PAN Number');
if (!application.gstNumber) missingFields.push('GST Number');
if (!application.bankName) missingFields.push('Bank Name');
if (!application.accountNumber) missingFields.push('Account Number');
if (!application.ifscCode) missingFields.push('IFSC Code');
if (decision === 'Approved' && missingFields.length > 0) {
return {
forbidden: true,
message: `Cannot approve LOA: Missing mandatory fields: ${missingFields.join(', ')}. Please ensure they are filled in the application details.`
};
}
targetStatus = APPLICATION_STATUS.LOA_ISSUED; targetStatus = APPLICATION_STATUS.LOA_ISSUED;
targetStage = 'LOA'; targetStage = 'LOA';
targetProgress = 95; targetProgress = 95;

View File

@ -63,7 +63,39 @@ const ACTION_DESCRIPTIONS: Record<string, string> = {
RESIGNATION_APPROVED: 'Resignation approved', RESIGNATION_APPROVED: 'Resignation approved',
RESIGNATION_REJECTED: 'Resignation rejected', RESIGNATION_REJECTED: 'Resignation rejected',
EMAIL_SENT: 'Email notification sent', EMAIL_SENT: 'Email notification sent',
REMINDER_SENT: 'Reminder sent' REMINDER_SENT: 'Reminder sent',
FDD_FLAGGED_NON_RESPONSIVE: 'APPLICANT FLAGGED: Non-responsive to audit queries'
};
const getNormalizedAuditPayload = (logData: any, entityType: string, entityId: string) => {
const payload = logData.details || logData.newData || {};
const actorName = logData.user?.fullName || logData.userName || 'System';
const action = logData.action || 'UPDATED';
let description = ACTION_DESCRIPTIONS[action] ||
String(action).split('_').map((w: any) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
if (payload?.stage) description += ` - Stage: ${payload.stage}`;
else if (payload?.department) description += ` - ${payload.department}`;
else if (payload?.status && action === 'UPDATED') description += ` to ${payload.status}`;
return {
id: logData.id,
action,
description,
entityType,
entityId,
actor: {
name: actorName,
email: logData.user?.email || logData.userEmail || null
},
userName: actorName,
userEmail: logData.user?.email || logData.userEmail || null,
remarks: logData.remarks || payload?.remarks || '',
newData: payload,
details: payload,
timestamp: logData.createdAt || logData.timestamp
};
}; };
/** /**
@ -94,8 +126,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
const type = (entityType as string).toLowerCase(); const type = (entityType as string).toLowerCase();
if (type === 'resignation') { if (type === 'resignation') {
const resignation = await db.Resignation.findOne({
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
attributes: ['id']
});
const resolvedResignationId = resignation?.id || (entityId as string);
const result = await db.ResignationAudit.findAndCountAll({ const result = await db.ResignationAudit.findAndCountAll({
where: { resignationId: entityId as string }, where: { resignationId: resolvedResignationId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -103,8 +140,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
count = result.count; count = result.count;
logs = result.rows; logs = result.rows;
} else if (type === 'termination') { } else if (type === 'termination') {
const termination = await db.TerminationRequest.findOne({
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedTerminationId = termination?.id || (entityId as string);
const result = await db.TerminationAudit.findAndCountAll({ const result = await db.TerminationAudit.findAndCountAll({
where: { terminationRequestId: entityId as string }, where: { terminationRequestId: resolvedTerminationId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -112,8 +154,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
count = result.count; count = result.count;
logs = result.rows; logs = result.rows;
} else if (type === 'fnf') { } else if (type === 'fnf') {
const fnf = await db.FnF.findOne({
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
attributes: ['id']
});
const resolvedFnfId = fnf?.id || (entityId as string);
const result = await db.FnFAudit.findAndCountAll({ const result = await db.FnFAudit.findAndCountAll({
where: { fnfId: entityId as string }, where: { fnfId: resolvedFnfId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -121,8 +168,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
count = result.count; count = result.count;
logs = result.rows; logs = result.rows;
} else if (type === 'constitutional_change') { } else if (type === 'constitutional_change') {
const constitutional = await db.ConstitutionalChange.findOne({
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
const result = await db.ConstitutionalAudit.findAndCountAll({ const result = await db.ConstitutionalAudit.findAndCountAll({
where: { constitutionalChangeId: entityId as string }, where: { constitutionalChangeId: resolvedConstitutionalId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
limit: limitNum, offset limit: limitNum, offset
@ -155,30 +207,10 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
// Format the response with human-readable descriptions and consistent mapping // Format the response with human-readable descriptions and consistent mapping
const formattedLogs = logs.map((log: any) => { const formattedLogs = logs.map((log: any) => {
const logData = log.get ? log.get({ plain: true }) : log; const logData = log.get ? log.get({ plain: true }) : log;
const details = logData.details || logData.newData; if (logData.details?.statutoryStatus === 'Flagged') {
logData.action = 'FDD_FLAGGED_NON_RESPONSIVE';
let baseDescription = ACTION_DESCRIPTIONS[logData.action] ||
logData.action.split('_').map((w: any) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
// EXCLUSIVE ADDITION: Enhance description with context if available (Stage/Status/Dept)
if (details) {
if (details.stage) baseDescription += ` - Stage: ${details.stage}`;
else if (details.department) baseDescription += ` - ${details.department}`;
else if (details.status && logData.action === 'UPDATED') baseDescription += ` to ${details.status}`;
} }
return getNormalizedAuditPayload(logData, entityType as string, entityId as string);
return {
id: logData.id,
action: logData.action,
description: baseDescription,
entityType: entityType,
entityId: entityId,
userName: logData.user?.fullName || 'System',
userEmail: logData.user?.email || null,
remarks: logData.remarks || logData.newData?.remarks,
newData: details, // Normalize module-specific 'details' to 'newData' for UI
timestamp: logData.createdAt
};
}); });
res.json({ res.json({
@ -217,30 +249,50 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
// Dynamic Table Switching // Dynamic Table Switching
if (type === 'resignation') { if (type === 'resignation') {
totalLogs = await db.ResignationAudit.count({ where: { resignationId: entityId as string } }); const resignation = await db.Resignation.findOne({
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
attributes: ['id']
});
const resolvedResignationId = resignation?.id || (entityId as string);
totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedResignationId } });
latestLog = await db.ResignationAudit.findOne({ latestLog = await db.ResignationAudit.findOne({
where: { resignationId: entityId as string }, where: { resignationId: resolvedResignationId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else if (type === 'termination') { } else if (type === 'termination') {
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: entityId as string } }); const termination = await db.TerminationRequest.findOne({
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedTerminationId = termination?.id || (entityId as string);
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedTerminationId } });
latestLog = await db.TerminationAudit.findOne({ latestLog = await db.TerminationAudit.findOne({
where: { terminationRequestId: entityId as string }, where: { terminationRequestId: resolvedTerminationId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else if (type === 'fnf') { } else if (type === 'fnf') {
totalLogs = await db.FnFAudit.count({ where: { fnfId: entityId as string } }); const fnf = await db.FnF.findOne({
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
attributes: ['id']
});
const resolvedFnfId = fnf?.id || (entityId as string);
totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedFnfId } });
latestLog = await db.FnFAudit.findOne({ latestLog = await db.FnFAudit.findOne({
where: { fnfId: entityId as string }, where: { fnfId: resolvedFnfId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
} else if (type === 'constitutional' || type === 'constitutional_change') { } else if (type === 'constitutional' || type === 'constitutional_change') {
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: entityId as string } }); const constitutional = await db.ConstitutionalChange.findOne({
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
attributes: ['id']
});
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedConstitutionalId } });
latestLog = await db.ConstitutionalAudit.findOne({ latestLog = await db.ConstitutionalAudit.findOne({
where: { constitutionalChangeId: entityId as string }, where: { constitutionalChangeId: resolvedConstitutionalId },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }], include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });

View File

@ -214,7 +214,7 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
if (checklist) { if (checklist) {
if (checklist.applicationId) { if (checklist.applicationId) {
await db.Application.update({ await db.Application.update({
overallStatus: 'Approved', overallStatus: 'Inauguration',
progressPercentage: 100 progressPercentage: 100
}, { where: { id: checklist.applicationId } }); }, { where: { id: checklist.applicationId } });

View File

@ -153,3 +153,38 @@ export const uploadReport = async (req: AuthRequest, res: Response) => {
res.status(500).json({ success: false, message: 'Error uploading report' }); 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' });
}
// 1. Update Application status at model level
await application.update({ statutoryStatus: 'Flagged' });
// 2. Add high-level Audit Log entry (Using literal 'UPDATED' to absolutely avoid ENUM errors)
console.log(`[FDDController] Flagging application ${application.id} with action: UPDATED`);
await AuditLog.create({
userId: req.user?.id,
action: 'UPDATED',
entityType: 'application',
entityId: application.id,
newData: { statutoryStatus: 'Flagged', remarks: remarks || 'Applicant is non-responsive to FDD queries.' }
});
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' });
}
};

View File

@ -8,5 +8,6 @@ router.use(authenticate as any);
router.get('/:applicationId', fddController.getAssignment); router.get('/:applicationId', fddController.getAssignment);
router.post('/assign', fddController.assignAgency); router.post('/assign', fddController.assignAgency);
router.post('/report', fddController.uploadReport); router.post('/report', fddController.uploadReport);
router.post('/flag', fddController.flagNonResponsive);
export default router; export default router;

View File

@ -9,6 +9,7 @@ import { AuthRequest } from '../../types/express.types.js';
import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js'; import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js';
import { syncLocationManagers } from '../master/syncHierarchy.service.js'; import { syncLocationManagers } from '../master/syncHierarchy.service.js';
import { WorkflowService } from '../../services/WorkflowService.js'; import { WorkflowService } from '../../services/WorkflowService.js';
import { WorkflowIntegrityService } from '../../services/WorkflowIntegrityService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js'; import { NomenclatureService } from '../../common/utils/nomenclature.js';
const { DocumentStageConfig } = db; const { DocumentStageConfig } = db;
@ -154,7 +155,8 @@ export const getApplications = async (req: AuthRequest, res: Response) => {
// Security Check: If prospective dealer, only show their own application // Security Check: If prospective dealer, only show their own application
if (req.user?.roleCode === 'Prospective Dealer') { if (req.user?.roleCode === 'Prospective Dealer') {
whereClause.email = req.user.email; // Filter by phone instead of email to show all applications from same user
whereClause.phone = (req.user as any).phone || req.user.email;
} }
// Security Check: If FDD user, only show applications where they are a participant // Security Check: If FDD user, only show applications where they are a participant
@ -197,6 +199,9 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
where.applicationId = targetId; where.applicationId = targetId;
} }
// PROACTIVE INTEGRITY CHECK: Ensure application isn't stalled before returning
await WorkflowIntegrityService.synchronizeApplicationState(targetId);
const application = await Application.findOne({ const application = await Application.findOne({
where, where,
include: [ include: [
@ -297,7 +302,19 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
// Security Check: Ensure prospective dealer controls data ownership and document privacy // Security Check: Ensure prospective dealer controls data ownership and document privacy
if (req.user?.roleCode === 'Prospective Dealer') { if (req.user?.roleCode === 'Prospective Dealer') {
if (application.email !== req.user.email) { const userEmail = req.user.email;
const userPhone = (req.user as any).phone;
// Helper to normalize phone for comparison (last 10 digits)
const normalize = (p: string) => p ? String(p).replace(/[^0-9]/g, '').slice(-10) : '';
const normalizedAppPhone = normalize(application.phone);
const normalizedUserPhone = normalize(userPhone);
const hasAccess =
(application.email && userEmail && application.email.toLowerCase() === userEmail.toLowerCase()) ||
(normalizedAppPhone && normalizedUserPhone && normalizedAppPhone === normalizedUserPhone);
if (!hasAccess) {
return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' }); return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' });
} }
@ -851,6 +868,34 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId } where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
}); });
if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
// Strict Workflow Validation: Dealer Code Generation requires LOI Issued status
if (application.overallStatus !== APPLICATION_STATUS.LOI_ISSUED && application.overallStatus !== APPLICATION_STATUS.DEALER_CODE_GENERATION) {
return res.status(400).json({
success: false,
message: `Cannot generate dealer codes. The application must be in 'LOI Issued' status (Current: ${application.overallStatus}).`
});
}
// Validation: Check for mandatory fields before triggering Dealer Code Generation
const mandatoryFields = [
{ key: 'panNumber', label: 'PAN Number' },
{ key: 'gstNumber', label: 'GST Number' },
{ key: 'bankName', label: 'Bank Name' },
{ key: 'accountNumber', label: 'Account Number' },
{ key: 'ifscCode', label: 'IFSC Code' }
];
const missingFields = mandatoryFields
.filter(f => !application[f.key as keyof typeof application])
.map(f => f.label);
if (missingFields.length > 0) {
return res.status(400).json({
success: false,
message: `Cannot generate dealer codes. Missing mandatory fields: ${missingFields.join(', ')}. Please update application details first.`
});
}
// Trigger Mock SAP Integration // Trigger Mock SAP Integration
const { data: sapData } = await ExternalMocksService.mockGenerateSapCodes(application.applicationId); const { data: sapData } = await ExternalMocksService.mockGenerateSapCodes(application.applicationId);

View File

@ -17,13 +17,15 @@ export class ProspectiveLoginController {
console.log(`[ProspectiveLogin] Received OTP request for phone: '${phone}'`); console.log(`[ProspectiveLogin] Received OTP request for phone: '${phone}'`);
// Check if application exists and is shortlisted // Check if ANY application exists for this phone and prioritize shortlisted ones
const application = await db.Application.findOne({ const application = await db.Application.findOne({
where: { phone: phone } where: { phone: phone },
order: [
[db.sequelize.literal("CASE WHEN \"isShortlisted\" = true OR \"ddLeadShortlisted\" = true THEN 0 ELSE 1 END"), 'ASC'],
['createdAt', 'DESC']
]
}); });
console.log(`[ProspectiveLogin] DB Search Result:`, application ? `Found AppId: ${application.id}, Shortlisted: ${application.isShortlisted}, DDLeadShortlisted: ${application.ddLeadShortlisted}` : 'Not Found');
if (!application) { if (!application) {
console.log(`[ProspectiveLogin] Application not found for ${phone}, returning 404`); console.log(`[ProspectiveLogin] Application not found for ${phone}, returning 404`);
return res.status(404).json({ message: 'No application found with this phone number' }); return res.status(404).json({ message: 'No application found with this phone number' });
@ -60,9 +62,13 @@ export class ProspectiveLoginController {
} }
if (otp === '123456') { if (otp === '123456') {
// Fetch application again to get details // Fetch the latest shortlisted application again to get details for the session
const application = await db.Application.findOne({ const application = await db.Application.findOne({
where: { phone: phone } where: { phone: phone },
order: [
[db.sequelize.literal("CASE WHEN \"isShortlisted\" = true OR \"ddLeadShortlisted\" = true THEN 0 ELSE 1 END"), 'ASC'],
['createdAt', 'DESC']
]
}); });
if (!application) { if (!application) {

View File

@ -1,7 +1,14 @@
import { Response, NextFunction } from 'express'; import { Response, NextFunction } from 'express';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
import logger from '../../common/utils/logger.js'; import logger from '../../common/utils/logger.js';
import { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js'; import {
RESIGNATION_STAGES,
AUDIT_ACTIONS,
ROLES,
REQUEST_TYPES,
RESIGNATION_DOCUMENT_TYPES,
RESIGNATION_DOCUMENT_STAGES
} from '../../common/config/constants.js';
import { Op, Transaction } from 'sequelize'; import { Op, Transaction } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js'; import ExternalMocksService from '../../common/utils/externalMocks.service.js';
@ -9,6 +16,7 @@ import ExternalMocksService from '../../common/utils/externalMocks.service.js';
import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js'; import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js'; import { NomenclatureService } from '../../common/utils/nomenclature.js';
import { ParticipantService } from '../../services/ParticipantService.js'; import { ParticipantService } from '../../services/ParticipantService.js';
import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
// Removed generateResignationId and moved to NomenclatureService // Removed generateResignationId and moved to NomenclatureService
@ -51,7 +59,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
reason, reason,
additionalInfo, additionalInfo,
currentStage: RESIGNATION_STAGES.ASM, currentStage: RESIGNATION_STAGES.ASM,
status: 'ASM Review', status: getResignationStatusForStage(RESIGNATION_STAGES.ASM),
progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM), progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM),
submittedOn: new Date(), submittedOn: new Date(),
documents: [], documents: [],
@ -184,6 +192,80 @@ export const getResignationById = async (req: AuthRequest, res: Response, next:
} }
}; };
export const uploadResignationDocument = async (req: AuthRequest, res: Response, next: NextFunction) => {
const transaction: Transaction = await db.sequelize.transaction();
try {
if (!req.user) throw new Error('Unauthorized');
if (!req.file) {
await transaction.rollback();
return res.status(400).json({ success: false, message: 'File is required' });
}
const { id } = req.params;
const { documentType = RESIGNATION_DOCUMENT_TYPES[0], stage = null } = req.body;
if (!RESIGNATION_DOCUMENT_TYPES.includes(documentType)) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: `Invalid document type. Allowed values: ${RESIGNATION_DOCUMENT_TYPES.join(', ')}`
});
}
if (stage && !RESIGNATION_DOCUMENT_STAGES.includes(stage)) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}`
});
}
const idStr = String(id);
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(idStr);
const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
});
if (!resignation) {
await transaction.rollback();
return res.status(404).json({ success: false, message: 'Resignation not found' });
}
const filePath = `/uploads/documents/${req.file.filename}`;
const document = await db.ResignationDocument.create({
resignationId: resignation.id,
documentType,
fileName: req.file.originalname,
filePath,
fileSize: req.file.size,
mimeType: req.file.mimetype,
stage,
uploadedBy: req.user.id
}, { transaction });
await db.ResignationAudit.create({
userId: req.user.id,
resignationId: resignation.id,
action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
remarks: `${documentType} uploaded`,
details: { fileName: req.file.originalname, stage, documentType }
}, { transaction });
const timeline = [...(resignation.timeline || []), {
stage: resignation.currentStage,
timestamp: new Date(),
user: req.user.fullName,
action: `Document uploaded: ${documentType}`,
remarks: req.file.originalname
}];
await resignation.update({ timeline }, { transaction });
await transaction.commit();
res.status(201).json({ success: true, message: 'Document uploaded successfully', document });
} catch (error) {
await transaction.rollback();
logger.error('Error uploading resignation document:', error);
next(error);
}
};
// Approve resignation (move to next stage) // Approve resignation (move to next stage)
export const approveResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { export const approveResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
const targetOverride = (req as any).targetStage; const targetOverride = (req as any).targetStage;
@ -237,7 +319,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
// Transition via Workflow Service // Transition via Workflow Service
await ResignationWorkflowService.transitionResignation(resignation, nextStage, req.user.id, { await ResignationWorkflowService.transitionResignation(resignation, nextStage, req.user.id, {
remarks, remarks,
status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review` status: getResignationStatusForStage(nextStage)
}); });
// Special logic for F&F and Completion // Special logic for F&F and Completion
@ -426,7 +508,7 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next:
await ResignationWorkflowService.transitionResignation(resignation, prevStage, req.user.id, { await ResignationWorkflowService.transitionResignation(resignation, prevStage, req.user.id, {
remarks, remarks,
action: 'Sent Back', action: 'Sent Back',
status: `${prevStage} Review (Sent Back)` status: `${getResignationStatusForStage(prevStage)} (Sent Back)`
}); });
await transaction.commit(); await transaction.commit();
@ -438,7 +520,107 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next:
} }
}; };
// Update departmental clearance // Update departmental clearance (existing code)...
// Manually assign participant
export const assignResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
if (!req.user) throw new Error('Unauthorized');
const { id } = req.params;
const { assignTo, remarks } = req.body; // assignTo is a role code or specific userId
const resignation = await db.Resignation.findOne({
where: { [Op.or]: [{ id }, { resignationId: id }] },
include: [{ model: db.User, as: 'dealer' }]
});
if (!resignation) {
return res.status(404).json({ success: false, message: 'Resignation not found' });
}
let targetUserId = null;
// If assignTo is a UUID, it's a direct user assignment
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(assignTo);
if (isUUID) {
targetUserId = assignTo;
} else {
// Role-based resolution
const user = await db.User.findByPk(resignation.dealerId);
if (user && user.dealerId) {
const dealer = await db.Dealer.findByPk(user.dealerId, {
include: [{
model: db.Application,
as: 'application',
include: [{
model: db.District,
as: 'district',
include: [{ model: db.Region, as: 'region' }, { model: db.Zone, as: 'zone' }]
}]
}]
});
if (dealer?.application?.district) {
const d = dealer.application.district;
if (assignTo === 'asm') targetUserId = d.asmId;
else if (assignTo === 'rbm') targetUserId = d.region?.rbmId;
else if (assignTo === 'zbh') targetUserId = d.zone?.zbhId;
}
}
// Fallback for national roles
if (!targetUserId) {
const roleIdMap: Record<string, string> = {
'nbh': ROLES.NBH,
'legal': ROLES.LEGAL_ADMIN,
'dd_admin': ROLES.DD_ADMIN
};
const targetRole = roleIdMap[assignTo];
if (targetRole) {
const roleUser = await db.User.findOne({ where: { roleCode: targetRole, status: 'active' } });
if (roleUser) targetUserId = roleUser.id;
}
}
}
if (!targetUserId) {
return res.status(400).json({
success: false,
message: `Could not resolve a unique user for assignment: ${assignTo}. Please ensure the underlying master data (District/Region/Zone) is correctly mapped.`
});
}
await db.RequestParticipant.findOrCreate({
where: {
requestId: resignation.id,
requestType: REQUEST_TYPES.RESIGNATION,
userId: targetUserId
},
defaults: {
participantType: 'contributor',
joinedMethod: 'manual',
metadata: {
assignedBy: req.user.id,
remarks: remarks || 'Manual assignment'
}
}
});
await db.ResignationAudit.create({
userId: req.user.id,
resignationId: resignation.id,
action: AUDIT_ACTIONS.UPDATED,
remarks: `Manually assigned user to the request. ${remarks || ''}`,
details: { assignedUserId: targetUserId, roleToAssign: assignTo }
});
res.json({ success: true, message: 'Participant assigned successfully' });
} catch (error) {
logger.error('Error assigning resignation:', error);
next(error);
}
};
export const updateClearance = async (req: AuthRequest, res: Response, next: NextFunction) => { export const updateClearance = async (req: AuthRequest, res: Response, next: NextFunction) => {
const transaction: Transaction = await db.sequelize.transaction(); const transaction: Transaction = await db.sequelize.transaction();
try { try {
@ -495,16 +677,8 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
// Sync with F&F Clearance if settlement exists // Sync with F&F Clearance if settlement exists
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } }); const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } });
if (fnf) { if (fnf) {
// Mapping UI status to F&F status
// Mapping status to F&F status based on amount logic
let fnfStatus = 'Pending';
const numAmount = parseFloat(amount) || 0; const numAmount = parseFloat(amount) || 0;
const fnfStatus = normalizeClearanceStatus(status, numAmount);
if (numAmount === 0) {
fnfStatus = 'NOC Submitted';
} else {
fnfStatus = 'Dues Pending';
}
const existingClearance = await db.FffClearance.findOne({ const existingClearance = await db.FffClearance.findOne({
where: { fnfId: fnf.id, department }, where: { fnfId: fnf.id, department },
@ -619,6 +793,9 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
(req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED; (req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED;
return approveResignation(req, res, next); return approveResignation(req, res, next);
case 'assign':
return assignResignation(req, res, next);
default: default:
return res.status(400).json({ return res.status(400).json({
success: false, success: false,

View File

@ -21,5 +21,6 @@ router.put('/:id/sendback', authenticate as any, resignationController.sendBackR
router.post('/:id/sendback', authenticate as any, resignationController.sendBackResignation); router.post('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
router.put('/:id/clearance', authenticate as any, uploadSingle, resignationController.updateClearance); router.put('/:id/clearance', authenticate as any, uploadSingle, resignationController.updateClearance);
router.post('/:id/documents', authenticate as any, uploadSingle, resignationController.uploadResignationDocument);
export default router; export default router;

View File

@ -5,6 +5,7 @@ import { AuthRequest } from '../../types/express.types.js';
import { FNF_STATUS, AUDIT_ACTIONS, FNF_DEPARTMENTS, RESIGNATION_STAGES, TERMINATION_STAGES } from '../../common/config/constants.js'; import { FNF_STATUS, AUDIT_ACTIONS, FNF_DEPARTMENTS, RESIGNATION_STAGES, TERMINATION_STAGES } from '../../common/config/constants.js';
import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js'; import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js'; import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
import { normalizeFnFStatus, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
export const getDepartments = async (req: Request, res: Response) => { export const getDepartments = async (req: Request, res: Response) => {
try { try {
@ -75,8 +76,9 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
const fnf = await FnF.findByPk(id); const fnf = await FnF.findByPk(id);
if (!fnf) return res.status(404).json({ success: false, message: 'F&F settlement not found' }); if (!fnf) return res.status(404).json({ success: false, message: 'F&F settlement not found' });
const normalizedStatus = normalizeFnFStatus(status || fnf.status);
await fnf.update({ await fnf.update({
status: status || fnf.status, status: normalizedStatus,
netAmount: finalSettlementAmount || fnf.netAmount, netAmount: finalSettlementAmount || fnf.netAmount,
settlementAmount: finalSettlementAmount || fnf.settlementAmount, settlementAmount: finalSettlementAmount || fnf.settlementAmount,
settlementDate: settlementDate || fnf.settlementDate, settlementDate: settlementDate || fnf.settlementDate,
@ -91,21 +93,29 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
action: AUDIT_ACTIONS.FNF_UPDATED, action: AUDIT_ACTIONS.FNF_UPDATED,
entityType: 'fnf', entityType: 'fnf',
entityId: id, entityId: id,
newData: { status, netAmount: finalSettlementAmount, remarks } newData: { status: normalizedStatus, netAmount: finalSettlementAmount, remarks }
}); });
// If status is being set to Completed, update the parent request status as well // If status is being set to Completed, transition parent request via workflow services
if (status === 'Completed' || status === FNF_STATUS.COMPLETED) { if (normalizedStatus === FNF_STATUS.COMPLETED) {
if (fnf.resignationId) { if (fnf.resignationId) {
await Resignation.update( const resignation = await Resignation.findByPk(fnf.resignationId);
{ status: 'Completed', stage: 'Completed' }, if (resignation) {
{ where: { id: fnf.resignationId } } await ResignationWorkflowService.transitionResignation(resignation, RESIGNATION_STAGES.COMPLETED, req.user?.id || null, {
); action: 'F&F Settlement Completed',
remarks: remarks || 'F&F marked completed from settlement module.',
status: 'Completed'
});
}
} else if (fnf.terminationRequestId) { } else if (fnf.terminationRequestId) {
await TerminationRequest.update( const termination = await TerminationRequest.findByPk(fnf.terminationRequestId);
{ status: 'Completed' }, if (termination) {
{ where: { id: fnf.terminationRequestId } } await TerminationWorkflowService.transitionTermination(termination, TERMINATION_STAGES.TERMINATED, req.user?.id || null, {
); action: 'F&F Settlement Completed',
remarks: remarks || 'F&F marked completed from settlement module.',
status: 'Terminated'
});
}
} }
} }
@ -305,7 +315,7 @@ const calculateFnFLogic = async (id: string, userId: string | null = null) => {
} }
// Determine Overall F&F Status // Determine Overall F&F Status
let newStatus = fnf.status; let newStatus = normalizeFnFStatus(fnf.status);
if (fnf.status === FNF_STATUS.INITIATED && progressPercentage > 0) { if (fnf.status === FNF_STATUS.INITIATED && progressPercentage > 0) {
newStatus = FNF_STATUS.DD_CLEARANCE; newStatus = FNF_STATUS.DD_CLEARANCE;
} }
@ -348,17 +358,21 @@ const calculateFnFLogic = async (id: string, userId: string | null = null) => {
export const updateClearance = async (req: AuthRequest, res: Response) => { export const updateClearance = async (req: AuthRequest, res: Response) => {
try { try {
const { id, clearanceId } = req.params; const { id, clearanceId } = req.params;
const { status, remarks, documentId, supportingDocument } = req.body; const body = (req.body || {}) as Record<string, any>;
const { status, remarks, documentId, supportingDocument } = body;
const clearance = await FffClearance.findOne({ where: { id: clearanceId, fnfId: id } }); const clearance = await FffClearance.findOne({ where: { id: clearanceId, fnfId: id } });
if (!clearance) return res.status(404).json({ success: false, message: 'Clearance record not found' }); if (!clearance) return res.status(404).json({ success: false, message: 'Clearance record not found' });
const uploadedSupportingDocument = req.file ? `/uploads/documents/${req.file.filename}` : undefined;
const normalizedStatus = normalizeClearanceStatus(status || clearance.status, Number(clearance.amount || 0));
await clearance.update({ await clearance.update({
status: status || clearance.status, status: normalizedStatus,
remarks: remarks || clearance.remarks, remarks: remarks || clearance.remarks,
documentId: documentId || clearance.documentId, documentId: documentId || clearance.documentId,
supportingDocument: supportingDocument || clearance.supportingDocument, supportingDocument: uploadedSupportingDocument || supportingDocument || clearance.supportingDocument,
clearedBy: req.user?.id, clearedBy: req.user?.id,
clearedAt: status === 'Cleared' ? new Date() : clearance.clearedAt clearedAt: normalizedStatus !== 'Pending' ? new Date() : clearance.clearedAt
}); });
// Automatically update FnF progress // Automatically update FnF progress
@ -373,7 +387,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
fnfId: id, fnfId: id,
action: 'CLEARANCE_UPDATED', action: 'CLEARANCE_UPDATED',
remarks: remarks || 'No remarks', remarks: remarks || 'No remarks',
details: { department: clearance.department, status } details: { department: clearance.department, status: normalizedStatus }
}); });
} catch (auditError) { } catch (auditError) {
console.error('[SettlementController] Local FnFAudit creation failed:', auditError); console.error('[SettlementController] Local FnFAudit creation failed:', auditError);
@ -393,7 +407,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
[parentKey]: parentId, [parentKey]: parentId,
action: 'STAKEHOLDER_CLEARANCE_UPDATED', action: 'STAKEHOLDER_CLEARANCE_UPDATED',
remarks: `Automated sync from F&F: ${remarks || 'No remarks'}`, remarks: `Automated sync from F&F: ${remarks || 'No remarks'}`,
details: { department: clearance.department, status } details: { department: clearance.department, status: normalizedStatus }
}); });
} catch (parentAuditError) { } catch (parentAuditError) {
console.error('[SettlementController] Parent Audit creation failed:', parentAuditError); console.error('[SettlementController] Parent Audit creation failed:', parentAuditError);

View File

@ -4,6 +4,7 @@ import * as settlementController from './settlement.controller.js';
import { authenticate } from '../../common/middleware/auth.js'; import { authenticate } from '../../common/middleware/auth.js';
import { checkRole } from '../../common/middleware/roleCheck.js'; import { checkRole } from '../../common/middleware/roleCheck.js';
import { ROLES } from '../../common/config/constants.js'; import { ROLES } from '../../common/config/constants.js';
import { uploadSingle } from '../../common/middleware/upload.js';
// All routes require authentication // All routes require authentication
router.use(authenticate as any); router.use(authenticate as any);
@ -19,7 +20,7 @@ router.put('/payments/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any
router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.updateFnF); router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.updateFnF);
router.post('/fnf/:id/calculate', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.calculateFnF); router.post('/fnf/:id/calculate', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.calculateFnF);
router.put('/fnf/:id/clearances/:clearanceId', checkRole([ROLES.FINANCE, ROLES.SPARES_MANAGER, ROLES.SERVICE_MANAGER, ROLES.ACCOUNTS_MANAGER, ROLES.SUPER_ADMIN]) as any, settlementController.updateClearance); router.put('/fnf/:id/clearances/:clearanceId', uploadSingle, checkRole([ROLES.FINANCE, ROLES.SPARES_MANAGER, ROLES.SERVICE_MANAGER, ROLES.ACCOUNTS_MANAGER, ROLES.SUPER_ADMIN]) as any, settlementController.updateClearance);
// Line item management // Line item management
router.post('/fnf/:id/line-items', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.addLineItem); router.post('/fnf/:id/line-items', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.addLineItem);

View File

@ -1,7 +1,13 @@
import { Response, NextFunction } from 'express'; import { Response, NextFunction } from 'express';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
import logger from '../../common/utils/logger.js'; import logger from '../../common/utils/logger.js';
import { TERMINATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js'; import {
TERMINATION_STAGES,
AUDIT_ACTIONS,
ROLES,
TERMINATION_DOCUMENT_TYPES,
TERMINATION_DOCUMENT_STAGES
} from '../../common/config/constants.js';
import { Op, Transaction } from 'sequelize'; import { Op, Transaction } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js'; import ExternalMocksService from '../../common/utils/externalMocks.service.js';
@ -9,6 +15,7 @@ import ExternalMocksService from '../../common/utils/externalMocks.service.js';
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js'; import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js'; import { NomenclatureService } from '../../common/utils/nomenclature.js';
import { ParticipantService } from '../../services/ParticipantService.js'; import { ParticipantService } from '../../services/ParticipantService.js';
import { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
// Create termination request // Create termination request
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => { export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
@ -27,7 +34,7 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
comments, comments,
initiatedBy: req.user.id, initiatedBy: req.user.id,
currentStage: TERMINATION_STAGES.SUBMITTED, currentStage: TERMINATION_STAGES.SUBMITTED,
status: 'Submitted', status: getTerminationStatusForStage(TERMINATION_STAGES.SUBMITTED),
progressPercentage: TerminationWorkflowService.calculateProgress(TERMINATION_STAGES.SUBMITTED), progressPercentage: TerminationWorkflowService.calculateProgress(TERMINATION_STAGES.SUBMITTED),
timeline: [{ timeline: [{
stage: 'Submitted', stage: 'Submitted',
@ -151,6 +158,80 @@ export const getTerminationById = async (req: AuthRequest, res: Response, next:
} }
}; };
export const uploadTerminationDocument = async (req: AuthRequest, res: Response, next: NextFunction) => {
const transaction: Transaction = await db.sequelize.transaction();
try {
if (!req.user) throw new Error('Unauthorized');
if (!req.file) {
await transaction.rollback();
return res.status(400).json({ success: false, message: 'File is required' });
}
const { id } = req.params;
const { documentType = TERMINATION_DOCUMENT_TYPES[0], stage = null } = req.body;
if (!TERMINATION_DOCUMENT_TYPES.includes(documentType)) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: `Invalid document type. Allowed values: ${TERMINATION_DOCUMENT_TYPES.join(', ')}`
});
}
if (stage && !TERMINATION_DOCUMENT_STAGES.includes(stage)) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}`
});
}
const idStr = String(id);
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(idStr);
const termination = await db.TerminationRequest.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
});
if (!termination) {
await transaction.rollback();
return res.status(404).json({ success: false, message: 'Termination request not found' });
}
const filePath = `/uploads/documents/${req.file.filename}`;
const document = await db.TerminationDocument.create({
terminationRequestId: termination.id,
documentType,
fileName: req.file.originalname,
filePath,
fileSize: req.file.size,
mimeType: req.file.mimetype,
stage,
uploadedBy: req.user.id
}, { transaction });
await db.TerminationAudit.create({
userId: req.user.id,
terminationRequestId: termination.id,
action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
remarks: `${documentType} uploaded`,
details: { fileName: req.file.originalname, stage, documentType }
}, { transaction });
const timeline = [...(termination.timeline || []), {
stage: termination.currentStage,
timestamp: new Date(),
user: req.user.fullName,
action: `Document uploaded: ${documentType}`,
remarks: req.file.originalname
}];
await termination.update({ timeline }, { transaction });
await transaction.commit();
res.status(201).json({ success: true, message: 'Document uploaded successfully', document });
} catch (error) {
await transaction.rollback();
logger.error('Error uploading termination document:', error);
next(error);
}
};
// Update termination status (Approve/Reject) // Update termination status (Approve/Reject)
export const updateTerminationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => { export const updateTerminationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => {
const transaction: Transaction = await db.sequelize.transaction(); const transaction: Transaction = await db.sequelize.transaction();
@ -197,7 +278,7 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
await TerminationWorkflowService.transitionTermination(termination, nextStage, req.user.id, { await TerminationWorkflowService.transitionTermination(termination, nextStage, req.user.id, {
remarks, remarks,
status: nextStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : `${nextStage}` status: getTerminationStatusForStage(nextStage)
}); });
// If Terminated, trigger F&F initiation via Workflow Service // If Terminated, trigger F&F initiation via Workflow Service
@ -215,8 +296,8 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
await TerminationWorkflowService.initiateFnF(termination, req.user.id, transaction); await TerminationWorkflowService.initiateFnF(termination, req.user.id, transaction);
} else { } else {
logger.info(`[TerminationController] Termination approved but LWD (${termination.proposedLwd}) not yet reached. F&F will be triggered on LWD.`); logger.info(`[TerminationController] Termination approved but LWD (${termination.proposedLwd}) not yet reached. F&F will be triggered on LWD.`);
// Update status to reflect F&F is pending LWD arrivement // Keep parent status aligned while waiting for LWD-triggered F&F
await termination.update({ status: 'Approved (F&F Pending LWD)' }, { transaction }); await termination.update({ status: 'Awaiting F&F (LWD Pending)' }, { transaction });
} }
} }
} }
@ -286,8 +367,9 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
if (!termination) throw new Error('Termination request not found'); if (!termination) throw new Error('Termination request not found');
const clearances = { ...(termination.departmentalClearances || {}) }; const clearances = { ...(termination.departmentalClearances || {}) };
const normalizedStatus = normalizeClearanceStatus(status, Number(amount) || 0);
clearances[department] = { clearances[department] = {
status, status: normalizedStatus,
amount: Number(amount) || 0, amount: Number(amount) || 0,
type: type || 'Receivable', type: type || 'Receivable',
remarks: remarks || '', remarks: remarks || '',
@ -301,7 +383,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
const fnf = await db.FnF.findOne({ where: { terminationRequestId: id } }); const fnf = await db.FnF.findOne({ where: { terminationRequestId: id } });
if (fnf) { if (fnf) {
await db.FffClearance.update( await db.FffClearance.update(
{ status, remarks, amount: Number(amount) || 0 }, { status: normalizedStatus, remarks, amount: Number(amount) || 0 },
{ where: { fnfId: fnf.id, department }, transaction } { where: { fnfId: fnf.id, department }, transaction }
); );
} }
@ -311,7 +393,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
action: 'CLEARANCE_UPDATED', action: 'CLEARANCE_UPDATED',
terminationRequestId: id, terminationRequestId: id,
remarks: remarks || `Cleared ${department}`, remarks: remarks || `Cleared ${department}`,
details: { department, status, amount } details: { department, status: normalizedStatus, amount }
}, { transaction }); }, { transaction });
if (fnf) { if (fnf) {
@ -320,7 +402,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
fnfId: fnf.id, fnfId: fnf.id,
action: 'CLEARANCE_UPDATED', action: 'CLEARANCE_UPDATED',
remarks: remarks || `Departmental clearance recorded for ${department}`, remarks: remarks || `Departmental clearance recorded for ${department}`,
details: { department, status, source: 'Termination Workflow' } details: { department, status: normalizedStatus, source: 'Termination Workflow' }
}, { transaction }); }, { transaction });
} }

View File

@ -2,9 +2,10 @@ import express from 'express';
const router = express.Router(); const router = express.Router();
import { import {
createTermination, getTerminations, getTerminationById, updateTerminationStatus, createTermination, getTerminations, getTerminationById, updateTerminationStatus,
submitScnResponse, recordPersonalHearing, updateClearance submitScnResponse, recordPersonalHearing, updateClearance, uploadTerminationDocument
} from './termination.controller.js'; } from './termination.controller.js';
import { authenticate } from '../../common/middleware/auth.js'; import { authenticate } from '../../common/middleware/auth.js';
import { uploadSingle } from '../../common/middleware/upload.js';
router.use(authenticate as any); router.use(authenticate as any);
@ -16,5 +17,6 @@ router.post('/:id/status', updateTerminationStatus);
router.post('/scn-response', submitScnResponse); router.post('/scn-response', submitScnResponse);
router.post('/hearing-record', recordPersonalHearing); router.post('/hearing-record', recordPersonalHearing);
router.put('/:id/clearance', updateClearance); router.put('/:id/clearance', updateClearance);
router.post('/:id/documents', uploadSingle, uploadTerminationDocument);
export default router; export default router;

View File

@ -7,11 +7,13 @@ export class ConstitutionalWorkflowService {
*/ */
static async transitionRequest(request: any, targetStage: string, userId: string, options: any = {}) { static async transitionRequest(request: any, targetStage: string, userId: string, options: any = {}) {
const { action, status, remarks, userFullName } = options; const { action, status, remarks, userFullName } = options;
const sourceStage = request.currentStage;
const updatedTimeline = [ const updatedTimeline = [
...request.timeline, ...(request.timeline || []),
{ {
stage: targetStage, stage: sourceStage, // Correctly Associate remark with the stage where action happened
targetStage: targetStage,
timestamp: new Date(), timestamp: new Date(),
user: userFullName || 'System', user: userFullName || 'System',
action: action || `Moved to ${targetStage}`, action: action || `Moved to ${targetStage}`,
@ -35,7 +37,7 @@ export class ConstitutionalWorkflowService {
constitutionalChangeId: request.id, constitutionalChangeId: request.id,
action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED, action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED,
remarks: remarks || '', remarks: remarks || '',
details: { status: updateData.status, stage: targetStage } details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
}); });
return request; return request;

View File

@ -25,13 +25,16 @@ export class RelocationWorkflowService {
updateData.progressPercentage = progressPercentage; updateData.progressPercentage = progressPercentage;
} }
const sourceStage = request.currentStage;
// 1. Update Request Record // 1. Update Request Record
await request.update(updateData); await request.update(updateData);
// 2. Update Timeline (JSON array) // 2. Update Timeline (JSON array)
const user = userId ? await User.findByPk(userId) : null; const user = userId ? await User.findByPk(userId) : null;
const timelineEntry = { const timelineEntry = {
stage: stage || request.currentStage, stage: sourceStage, // Store the stage where the action happened
targetStage: stage || targetStatus,
timestamp: new Date(), timestamp: new Date(),
user: user ? user.fullName : 'System', user: user ? user.fullName : 'System',
action: action || `Transitioned to ${targetStatus}`, action: action || `Transitioned to ${targetStatus}`,
@ -47,7 +50,7 @@ export class RelocationWorkflowService {
relocationRequestId: request.id, relocationRequestId: request.id,
action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED, action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED,
remarks: reason || '', remarks: reason || '',
details: { status: targetStatus, stage: stage || request.currentStage } details: { status: targetStatus, stage: sourceStage, targetStage: stage || targetStatus }
}); });
console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`); console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`);

View File

@ -1,6 +1,7 @@
import db from '../database/models/index.js'; import db from '../database/models/index.js';
const { AuditLog, User, Worknote } = db; const { AuditLog, User, Worknote } = db;
import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js'; import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js';
import { getResignationStatusForStage } from '../common/utils/offboardingStatus.js';
import { NotificationService } from './NotificationService.js'; import { NotificationService } from './NotificationService.js';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import logger from '../common/utils/logger.js'; import logger from '../common/utils/logger.js';
@ -12,21 +13,20 @@ export class ResignationWorkflowService {
*/ */
static async transitionResignation(resignation: any, targetStage: string, userId: string | null = null, metadata: any = {}) { static async transitionResignation(resignation: any, targetStage: string, userId: string | null = null, metadata: any = {}) {
const { action, remarks, status } = metadata; const { action, remarks, status } = metadata;
const sourceStage = resignation.currentStage;
const updateData: any = { const updateData: any = {
currentStage: targetStage, currentStage: targetStage,
status: status || targetStage, status: status || getResignationStatusForStage(targetStage),
progressPercentage: this.calculateProgress(targetStage), progressPercentage: this.calculateProgress(targetStage),
updatedAt: new Date() updatedAt: new Date()
}; };
// 1. Update Resignation Record // 2. Update Timeline (JSON array) & Resignation Record
await resignation.update(updateData);
// 2. Update Timeline (JSON array)
const actor = userId ? await User.findByPk(userId) : null; const actor = userId ? await User.findByPk(userId) : null;
const timelineEntry = { const timelineEntry = {
stage: targetStage, stage: sourceStage, // Correctly Associate remark with the stage where action happened
targetStage: targetStage, // Store target for reference
timestamp: new Date(), timestamp: new Date(),
user: actor ? actor.fullName : 'System', user: actor ? actor.fullName : 'System',
action: action || `Approved to ${targetStage}`, action: action || `Approved to ${targetStage}`,
@ -34,7 +34,11 @@ export class ResignationWorkflowService {
}; };
const updatedTimeline = [...(resignation.timeline || []), timelineEntry]; const updatedTimeline = [...(resignation.timeline || []), timelineEntry];
await resignation.update({ timeline: updatedTimeline });
await resignation.update({
...updateData,
timeline: updatedTimeline
});
// 3. Create Audit Log // 3. Create Audit Log
let auditAction: any = AUDIT_ACTIONS.APPROVED; let auditAction: any = AUDIT_ACTIONS.APPROVED;
@ -47,7 +51,7 @@ export class ResignationWorkflowService {
resignationId: resignation.id, resignationId: resignation.id,
action: auditAction, action: auditAction,
remarks: remarks || '', remarks: remarks || '',
details: { status: updateData.status, stage: targetStage } details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
}); });
// 4. Create Worknote if it's a "Sent Back" action for communication // 4. Create Worknote if it's a "Sent Back" action for communication

View File

@ -2,6 +2,7 @@ import db from '../database/models/index.js';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db; const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db;
import { AUDIT_ACTIONS, TERMINATION_STAGES, ROLES, FNF_DEPARTMENTS } from '../common/config/constants.js'; import { AUDIT_ACTIONS, TERMINATION_STAGES, ROLES, FNF_DEPARTMENTS } from '../common/config/constants.js';
import { getTerminationStatusForStage } from '../common/utils/offboardingStatus.js';
import { NotificationService } from './NotificationService.js'; import { NotificationService } from './NotificationService.js';
import ExternalMocksService from '../common/utils/externalMocks.service.js'; import ExternalMocksService from '../common/utils/externalMocks.service.js';
import logger from '../common/utils/logger.js'; import logger from '../common/utils/logger.js';
@ -13,20 +14,22 @@ export class TerminationWorkflowService {
*/ */
static async transitionTermination(termination: any, targetStage: string, userId: string | null = null, metadata: any = {}) { static async transitionTermination(termination: any, targetStage: string, userId: string | null = null, metadata: any = {}) {
const { action, remarks, status } = metadata; const { action, remarks, status } = metadata;
const sourceStage = termination.currentStage;
const updateData: any = { const updateData: any = {
currentStage: targetStage, currentStage: targetStage,
status: status || (targetStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : targetStage), status: status || getTerminationStatusForStage(targetStage),
progressPercentage: this.calculateProgress(targetStage),
updatedAt: new Date() updatedAt: new Date()
}; };
// 1. Update Termination Record // 1. Resolve Actor
await termination.update(updateData);
// 2. Update Timeline (JSON array)
const actor = userId ? await User.findByPk(userId) : null; const actor = userId ? await User.findByPk(userId) : null;
// 2. Prepare Timeline Entry
const timelineEntry = { const timelineEntry = {
stage: targetStage, stage: sourceStage, // Correctly Associate remark with the stage where action happened
targetStage: targetStage,
timestamp: new Date(), timestamp: new Date(),
user: actor ? actor.fullName : 'System', user: actor ? actor.fullName : 'System',
action: action || `Approved to ${targetStage}`, action: action || `Approved to ${targetStage}`,
@ -34,9 +37,14 @@ export class TerminationWorkflowService {
}; };
const updatedTimeline = [...(termination.timeline || []), timelineEntry]; const updatedTimeline = [...(termination.timeline || []), timelineEntry];
await termination.update({ timeline: updatedTimeline });
// 3. Create Audit Log // 3. Perform Consolidated Update
await termination.update({
...updateData,
timeline: updatedTimeline
});
// 4. Create Audit Log
let auditAction: any = AUDIT_ACTIONS.APPROVED; let auditAction: any = AUDIT_ACTIONS.APPROVED;
if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED; if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED;
if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED; if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED;
@ -46,7 +54,7 @@ export class TerminationWorkflowService {
terminationRequestId: termination.id, terminationRequestId: termination.id,
action: auditAction, action: auditAction,
remarks: remarks || '', remarks: remarks || '',
details: { status: updateData.status, stage: targetStage } details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
}); });
// 4. Send Notifications // 4. Send Notifications

View File

@ -0,0 +1,131 @@
import db from '../database/models/index.js';
import { WorkflowService } from './WorkflowService.js';
import { APPLICATION_STATUS } from '../common/config/constants.js';
const LOA_STAGE_CODE = 'LOA_APPROVAL';
const LOI_STAGE_CODE = 'LOI_APPROVAL';
export class WorkflowIntegrityService {
/**
* Ensures an application's state is consistent with its approvals and policies.
* This fixes stalled applications automatically without manual "healing" scripts.
*/
static async synchronizeApplicationState(applicationId: string) {
try {
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(applicationId);
const application = await db.Application.findOne({
where: isUUID ? { [db.Sequelize.Op.or]: [{ id: applicationId }, { applicationId }] } : { applicationId }
});
if (!application) return;
// LOA STAGE INTEGRITY
if (['LOA Pending', 'LOA Issued'].includes(application.overallStatus)) {
await this.syncLoaIntegrity(application);
}
// LOI STAGE INTEGRITY
if ([APPLICATION_STATUS.LOI_IN_PROGRESS, APPLICATION_STATUS.PAYMENT_PENDING, APPLICATION_STATUS.SECURITY_DETAILS].includes(application.overallStatus)) {
await this.syncLoiIntegrity(application);
}
// DEALER CODE INTEGRITY (Parallelizing Architecture/Statutory)
if ([APPLICATION_STATUS.DEALER_CODE_GENERATION, APPLICATION_STATUS.ARCHITECTURE_TEAM_ASSIGNED, APPLICATION_STATUS.ARCHITECTURE_DOCUMENT_UPLOAD, APPLICATION_STATUS.ARCHITECTURE_TEAM_COMPLETION].includes(application.overallStatus) || application.overallStatus.includes('Statutory')) {
await this.syncDealerCodeIntegrity(application);
}
// Other stages can be added here...
} catch (error) {
console.error(`[WorkflowIntegrityService] Error syncing app ${applicationId}:`, error);
}
}
private static async syncDealerCodeIntegrity(application: any) {
// Check if Dealer codes exist
const dealerCode = await db.DealerCode.findOne({
where: { applicationId: application.id, status: 'Active' }
});
if (dealerCode) {
console.log(`[WorkflowIntegrityService] Dealer Codes exist for ${application.applicationId}. Transitioning to LOA Pending...`);
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOA_PENDING, null, {
reason: 'Auto-transitioned by Integrity Service: Dealer codes generated. Architecture/Statutory work will proceed in parallel.',
progressPercentage: 85
});
}
}
private static async syncLoaIntegrity(application: any) {
// 1. Migrate legacy approvals if they exist but aren't in the new system
const legacyApprovals = await db.LoaApproval.findAll({
where: { action: 'Approved' },
include: [{
model: db.LoaRequest,
as: 'request',
where: { applicationId: application.id }
}]
});
for (const legacy of legacyApprovals) {
if (legacy.approverId) {
await db.StageApprovalAction.findOrCreate({
where: {
applicationId: application.id,
stageCode: LOA_STAGE_CODE,
actorUserId: legacy.approverId
},
defaults: {
actorRole: legacy.approverRole,
decision: 'Approved',
remarks: legacy.remarks || 'Migrated from legacy approval'
}
});
}
}
// 2. Evaluate policy and transition if met
const { policyMet } = await WorkflowService.evaluateStagePolicy(application.id, LOA_STAGE_CODE);
if (policyMet && application.overallStatus !== APPLICATION_STATUS.EOR_IN_PROGRESS) {
console.log(`[WorkflowIntegrityService] Policy met for LOA on ${application.applicationId}. Transitioning...`);
// Ensure LoaRequest is also updated
const request = await db.LoaRequest.findOne({ where: { applicationId: application.id } });
if (request && request.status !== 'Approved') {
await request.update({ status: 'Approved', approvedAt: new Date() });
}
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.EOR_IN_PROGRESS, null, {
reason: 'Auto-transitioned by Integrity Service: Approval condition satisfied.',
progressPercentage: 97
});
}
}
private static async syncLoiIntegrity(application: any) {
// 1. Evaluate policy
const { policyMet } = await WorkflowService.evaluateStagePolicy(application.id, LOI_STAGE_CODE);
// 2. Check for Security Deposit verification
const deposit = await db.SecurityDeposit.findOne({
where: { applicationId: application.id, depositType: 'SECURITY_DEPOSIT', status: 'Verified' }
});
if (policyMet && deposit) {
console.log(`[WorkflowIntegrityService] Policy met and Payment Verified for LOI on ${application.applicationId}. Transitioning to LOI Issued...`);
// Ensure LoiRequest is also updated
const request = await db.LoiRequest.findOne({ where: { applicationId: application.id } });
if (request && request.status !== 'Approved') {
await request.update({ status: 'Approved', approvedAt: new Date() });
}
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_ISSUED, null, {
reason: 'Auto-transitioned by Integrity Service: Policy and Payment criteria met.',
progressPercentage: 80
});
}
}
}

View File

@ -1,7 +1,11 @@
import fs from 'fs'; const args = Object.fromEntries(
process.argv.slice(2)
const BASE_URL = 'http://localhost:5000/api'; .map(arg => arg.replace(/^--/, '').split('='))
.map(([k, v]) => [k, v ?? 'true'])
);
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
const PASSWORD = 'Admin@123'; const PASSWORD = 'Admin@123';
const STEP_DELAY_MS = Number(args.delayMs || 500);
const EMAILS = { const EMAILS = {
DD_ADMIN: 'lince@gmail.com', DD_ADMIN: 'lince@gmail.com',
@ -45,11 +49,14 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
} }
async function login(email) { async function login(email) {
if (!login.cache) login.cache = {};
if (login.cache[email]) return login.cache[email];
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD }); const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
return data.token; login.cache[email] = data.token;
return login.cache[email];
} }
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms)); const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
async function run() { async function run() {
try { try {
@ -58,17 +65,21 @@ async function run() {
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`); console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`);
const dealerToken = await login(EMAILS.DEALER); const dealerToken = await login(EMAILS.DEALER);
console.log('[STEP 1] Dealer Submitting Constitutional Change...'); let requestId = args.requestId;
const createRes = await apiRequest('/self-service/constitutional', 'POST', { if (!requestId) {
changeType: 'LLP Conversion', console.log('[STEP 1] Dealer Submitting Constitutional Change...');
reason: 'Converting to LLP for better operational governance.', const createRes = await apiRequest('/self-service/constitutional', 'POST', {
currentConstitution: 'Proprietorship', changeType: args.changeType || 'LLP Conversion',
newPartnersDetails: 'John Doe, Jane Smith', reason: args.reason || 'Converting to LLP for better operational governance.',
shareholdingPattern: '60/40' currentConstitution: 'Proprietorship',
}, dealerToken); newPartnersDetails: 'John Doe, Jane Smith',
shareholdingPattern: '60/40'
const requestId = createRes.requestId; }, dealerToken);
console.log(`[STEP 1] Request Created. RequestID: ${requestId}`); requestId = createRes.requestId;
console.log(`[STEP 1] Request Created. RequestID: ${requestId}`);
} else {
console.log(`[STEP 1] Resuming request: ${requestId}`);
}
// Sequence of users taking actions to advance stages // Sequence of users taking actions to advance stages
const approvalSequence = [ const approvalSequence = [
@ -82,8 +93,15 @@ async function run() {
{ name: 'Legal Finalize', email: EMAILS.LEGAL } { name: 'Legal Finalize', email: EMAILS.LEGAL }
]; ];
let currentStep = 2; const adminToken = await login(EMAILS.DD_ADMIN);
for (const actor of approvalSequence) { const current = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
const currentStage = current?.request?.currentStage;
const stageOrder = ['Submitted', 'ASM Review', 'ZM/RBM Review', 'ZBH Review', 'DD Lead Review', 'DD Head Review', 'NBH Approval', 'Legal Review', 'Completed'];
const startIndex = Math.max(0, stageOrder.indexOf(currentStage));
let currentStep = 2 + startIndex;
for (let i = startIndex; i < approvalSequence.length; i++) {
const actor = approvalSequence[i];
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`); console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`);
const token = await login(actor.email); const token = await login(actor.email);
const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', { const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', {
@ -96,7 +114,6 @@ async function run() {
} }
console.log('[FINAL STEP] Verifying Completion Status...'); console.log('[FINAL STEP] Verifying Completion Status...');
const adminToken = await login(EMAILS.DD_ADMIN);
const finalDetails = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken); const finalDetails = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') { if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') {

156
trigger-relocation.js Normal file
View File

@ -0,0 +1,156 @@
const args = Object.fromEntries(
process.argv.slice(2)
.map((arg) => arg.replace(/^--/, "").split("="))
.map(([k, v]) => [k, v ?? "true"])
);
const BASE_URL = args.baseUrl || process.env.BASE_URL || "http://localhost:5000/api";
const PASSWORD = "Admin@123";
const STEP_DELAY_MS = Number(args.delayMs || 500);
const EMAILS = {
DD_ADMIN: args.ddAdminEmail || "lince@gmail.com",
DEALER: args.dealerEmail || "dealer@royalenfield.com",
ASM: args.asmEmail || "asm.sdelhi@royalenfield.com",
RBM: args.rbmEmail || "rbm.ncr@royalenfield.com",
DD_ZM: args.ddZmEmail || "ddzm.ncr@royalenfield.com",
ZBH: args.zbhEmail || "yashwin@gmail.com",
DD_LEAD: args.ddLeadEmail || "ddlead@royalenfield.com",
DD_HEAD: args.ddHeadEmail || "ddhead@royalenfield.com",
NBH: args.nbhEmail || "nbh@royalenfield.com",
LEGAL: args.legalEmail || "legal@royalenfield.com",
};
const ROLE_BY_STAGE = {
"ASM Review": ["ASM"],
"RBM Review": ["RBM"],
"DD ZM Review": ["DD_ZM", "RBM"],
"ZBH Review": ["ZBH"],
"DD Lead Review": ["DD_LEAD"],
"DD Head Approval": ["DD_HEAD"],
"NBH Approval": ["NBH"],
"Legal Clearance": ["LEGAL"],
"NBH Clearance with EOR": ["NBH"],
};
const delay = (ms = STEP_DELAY_MS) => new Promise((r) => setTimeout(r, ms));
async function apiRequest(endpoint, method = "GET", body = null, token = null) {
const headers = { "Content-Type": "application/json" };
if (token) headers.Authorization = `Bearer ${token}`;
const config = { method, headers };
if (body) config.body = JSON.stringify(body);
const response = await fetch(`${BASE_URL}${endpoint}`, config);
const data = await response.json();
if (!response.ok) {
throw new Error(`API Error ${method} ${endpoint}: ${JSON.stringify(data)}`);
}
return data;
}
async function login(email) {
if (!login.cache) login.cache = {};
if (login.cache[email]) return login.cache[email];
const data = await apiRequest("/auth/login", "POST", { email, password: PASSWORD });
login.cache[email] = data.token;
return data.token;
}
async function getRelocationByAnyId(id, token) {
return apiRequest(`/self-service/relocation/${id}`, "GET", null, token);
}
async function approveCurrentStage(requestId, stageName) {
const candidateRoles = ROLE_BY_STAGE[stageName] || [];
if (!candidateRoles.length) {
throw new Error(`No actor mapping found for stage: ${stageName}`);
}
let lastError = null;
for (const roleKey of candidateRoles) {
const email = EMAILS[roleKey];
if (!email) continue;
try {
const token = await login(email);
const res = await apiRequest(`/self-service/relocation/${requestId}/action`, "POST", {
action: "APPROVE",
comments: `${roleKey} approved relocation request.`,
}, token);
return { roleKey, email, message: res.message || "Approved" };
} catch (error) {
lastError = error;
}
}
throw lastError || new Error(`Approval failed for stage: ${stageName}`);
}
async function resolveDealerOutlet(dealerToken) {
const dashboard = await apiRequest("/dealer/dashboard", "GET", null, dealerToken);
const outlet = dashboard?.data?.outlets?.[0];
if (!outlet) throw new Error("No dealer outlet found to create relocation request.");
return outlet;
}
async function run() {
try {
console.log("--- STARTING RELOCATION E2E FLOW ---");
const adminToken = await login(EMAILS.DD_ADMIN);
const dealerToken = await login(EMAILS.DEALER);
let requestId = args.requestId;
if (!requestId) {
const outlet = await resolveDealerOutlet(dealerToken);
console.log(`[STEP 1] Submitting relocation for outlet: ${outlet.name} (${outlet.id})`);
const createRes = await apiRequest("/self-service/relocation", "POST", {
outletId: args.outletId || outlet.id,
relocationType: args.relocationType || "Intercity",
newAddress: args.newAddress || "Sector 21, New Premises",
newCity: args.newCity || "Gurugram",
newState: args.newState || "Haryana",
reason: args.reason || "Business expansion and better customer access",
proposedDate: args.proposedDate || new Date().toISOString().split("T")[0],
}, dealerToken);
requestId = createRes.requestId;
console.log(`[STEP 1] Request Created: ${requestId}`);
await delay();
} else {
console.log(`[STEP 1] Resuming request: ${requestId}`);
}
let step = 2;
while (true) {
const detailsRes = await getRelocationByAnyId(requestId, adminToken);
const request = detailsRes.request;
const stage = request.currentStage;
const status = request.status;
if (stage === "Completed" || status === "Completed") {
console.log(`[STEP ${step}] SUCCESS: Relocation request is completed.`);
break;
}
if (stage === "Rejected" || status === "Rejected") {
throw new Error(`Relocation request is rejected at stage: ${stage}`);
}
console.log(`[STEP ${step}] Current Stage: ${stage} | Status: ${status}`);
const actor = await approveCurrentStage(requestId, stage);
console.log(`[STEP ${step}] ${actor.roleKey} (${actor.email}) -> ${actor.message}`);
step++;
await delay();
}
const finalRes = await getRelocationByAnyId(requestId, adminToken);
console.log("--- VERIFICATION RESULTS ---");
console.log(`RequestId: ${finalRes.request.requestId || requestId}`);
console.log(`Final Stage: ${finalRes.request.currentStage}`);
console.log(`Final Status: ${finalRes.request.status}`);
console.log("Outcome: RELOCATION FLOW COMPLETED SUCCESSFULLY");
process.exit(0);
} catch (error) {
console.error("Workflow failed:", error.message);
process.exit(1);
}
}
run();

View File

@ -1,7 +1,13 @@
import fs from 'fs'; const args = Object.fromEntries(
process.argv.slice(2)
const BASE_URL = 'http://localhost:5000/api'; .map(arg => arg.replace(/^--/, '').split('='))
.map(([k, v]) => [k, v ?? 'true'])
);
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
const PASSWORD = 'Admin@123'; const PASSWORD = 'Admin@123';
const STEP_DELAY_MS = Number(args.delayMs || 500);
const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true';
const SHOULD_SKIP_FINAL_SETTLEMENT = String(args.skipSettlement || 'false') === 'true';
const EMAILS = { const EMAILS = {
DD_ADMIN: 'lince@gmail.com', DD_ADMIN: 'lince@gmail.com',
@ -44,16 +50,19 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
} }
async function login(email) { async function login(email) {
if (!login.cache) login.cache = {};
if (login.cache[email]) return login.cache[email];
const isInternal = email.endsWith('@royalenfield.com') || const isInternal = email.endsWith('@royalenfield.com') ||
email === 'lince@gmail.com' || email === 'lince@gmail.com' ||
email === 'yashwin@gmail.com'; email === 'yashwin@gmail.com';
const password = isInternal ? 'Admin@123' : 'Dealer@123'; const password = isInternal ? 'Admin@123' : 'Dealer@123';
const data = await apiRequest('/auth/login', 'POST', { email, password }); const data = await apiRequest('/auth/login', 'POST', { email, password });
return data.token; login.cache[email] = data.token;
return login.cache[email];
} }
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms)); const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`); const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
async function run() { async function run() {
@ -101,29 +110,32 @@ async function run() {
console.log(`Found Target Outlet: ${targetOutlet.name} (${targetOutlet.code})`); console.log(`Found Target Outlet: ${targetOutlet.name} (${targetOutlet.code})`);
console.log(`[STEP 1.2] Dealer Submitting Resignation for Outlet...`); console.log(`[STEP 1.2] Dealer Submitting Resignation for Outlet...`);
let resignationId; let resignationId = args.resignationId;
try { if (!resignationId) {
const createRes = await apiRequest('/self-service/resignations', 'POST', { try {
outletId: targetOutlet.id, const createRes = await apiRequest('/self-service/resignations', 'POST', {
resignationType: 'Voluntary', outletId: targetOutlet.id,
lastOperationalDateSales: new Date().toISOString().split('T')[0], resignationType: 'Voluntary',
lastOperationalDateServices: new Date().toISOString().split('T')[0], lastOperationalDateSales: new Date().toISOString().split('T')[0],
reason: 'Focusing on other business ventures', lastOperationalDateServices: new Date().toISOString().split('T')[0],
remarks: 'Initiating voluntary resignation for E2E validation.' reason: 'Focusing on other business ventures',
}, dealerToken); remarks: 'Initiating voluntary resignation for E2E validation.'
resignationId = createRes.resignation.id; }, dealerToken);
log(1, `Resignation Created. ID: ${resignationId}`); resignationId = createRes.resignation.id;
} catch (e) { log(1, `Resignation Created. ID: ${resignationId}`);
if (e.message.includes('already has an active resignation request')) { } catch (e) {
console.log(`[STEP 1.2] Active resignation already exists. Fetching...`); if (e.message.includes('already has an active resignation request')) {
// Use plural route for listing console.log(`[STEP 1.2] Active resignation already exists. Fetching...`);
const activeResRes = await apiRequest('/self-service/resignations', 'GET', null, dealerToken); const activeResRes = await apiRequest('/self-service/resignations', 'GET', null, dealerToken);
const activeRes = (activeResRes.resignations || activeResRes.data).find(r => r.outletId === targetOutlet.id && !['Completed', 'Rejected'].includes(r.status)); const activeRes = (activeResRes.resignations || activeResRes.data).find(r => r.outletId === targetOutlet.id && !['Completed', 'Rejected'].includes(r.status));
resignationId = activeRes.id; resignationId = activeRes.id;
log(1, `Resuming with existig Resignation: ${resignationId}`); log(1, `Resuming with existing Resignation: ${resignationId}`);
} else { } else {
throw e; throw e;
}
} }
} else {
log(1, `Resuming provided resignation: ${resignationId}`);
} }
await delay(); await delay();
@ -165,7 +177,9 @@ async function run() {
} }
// --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) --- // --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) ---
console.log('[STEP 9] Starting 16-Department F&F Clearance Flow...'); if (!SHOULD_SKIP_CLEARANCES) {
console.log('[STEP 9] Starting 16-Department F&F Clearance Flow...');
}
// Re-fetch to ensure we have the F&F ID regardless of start point // Re-fetch to ensure we have the F&F ID regardless of start point
const finalResData = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken); const finalResData = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken);
@ -199,32 +213,36 @@ async function run() {
{ name: 'Customer Relations Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'Customer complaints resolved.' } { name: 'Customer Relations Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'Customer complaints resolved.' }
]; ];
for (const dept of departments) { if (!SHOULD_SKIP_CLEARANCES) {
log('9.1', `Clearing Dept: ${dept.name} [${dept.status}] - ${dept.remarks}`); for (const dept of departments) {
await apiRequest(`/self-service/resignations/${resignationId}/clearance`, 'PUT', { log('9.1', `Clearing Dept: ${dept.name} [${dept.status}] - ${dept.remarks}`);
department: dept.name, await apiRequest(`/self-service/resignations/${resignationId}/clearance`, 'PUT', {
status: dept.status, department: dept.name,
remarks: dept.remarks, status: dept.status,
amount: dept.amount, remarks: dept.remarks,
type: dept.type amount: dept.amount,
}, adminToken); type: dept.type
await delay(100); }, adminToken);
await delay(100);
}
log(9, 'All 16 Departments Cleared.');
await delay();
} }
log(9, 'All 16 Departments Cleared.');
await delay();
// --- FINAL FINANCE SETTLEMENT --- // --- FINAL FINANCE SETTLEMENT ---
console.log('[STEP 10] Finance Finalizing Settlement...'); if (!SHOULD_SKIP_FINAL_SETTLEMENT) {
const financeToken = await login(EMAILS.FINANCE); console.log('[STEP 10] Finance Finalizing Settlement...');
await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', { const financeToken = await login(EMAILS.FINANCE);
status: 'Completed', await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', {
finalSettlementAmount: 415173, // Matches your observed amount status: 'Completed',
paymentMode: 'NEFT / Bank Transfer', finalSettlementAmount: Number(args.finalSettlementAmount || 415173),
transactionReference: `TXN-${Date.now()}`, paymentMode: 'NEFT / Bank Transfer',
settlementDate: new Date().toISOString(), transactionReference: `TXN-${Date.now()}`,
remarks: 'Settlement completed and verified via automated script.' settlementDate: new Date().toISOString(),
}, financeToken); remarks: 'Settlement completed and verified via automated script.'
await delay(); }, financeToken);
await delay();
}
// --- FINAL COMPLETION --- // --- FINAL COMPLETION ---
console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...'); console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...');

View File

@ -1,7 +1,12 @@
import fs from 'fs'; const args = Object.fromEntries(
process.argv.slice(2)
const BASE_URL = 'http://localhost:5000/api'; .map(arg => arg.replace(/^--/, '').split('='))
.map(([k, v]) => [k, v ?? 'true'])
);
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
const PASSWORD = 'Admin@123'; const PASSWORD = 'Admin@123';
const STEP_DELAY_MS = Number(args.delayMs || 500);
const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true';
const EMAILS = { const EMAILS = {
DD_ADMIN: 'lince@gmail.com', DD_ADMIN: 'lince@gmail.com',
@ -44,11 +49,14 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
} }
async function login(email) { async function login(email) {
if (!login.cache) login.cache = {};
if (login.cache[email]) return login.cache[email];
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD }); const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
return data.token; login.cache[email] = data.token;
return login.cache[email];
} }
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms)); const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`); const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
async function run() { async function run() {
@ -63,19 +71,26 @@ async function run() {
console.log(`Targeting Dealer: ${targetDealer.legalName} (${targetDealer.id})`); console.log(`Targeting Dealer: ${targetDealer.legalName} (${targetDealer.id})`);
// STEP 1: Submission (ASM) let terminationId = args.terminationId;
console.log('[STEP 1] ASM Initiating Termination...'); if (!terminationId) {
const asmToken = await login(EMAILS.ASM); console.log('[STEP 1] ASM Initiating Termination...');
const createRes = await apiRequest('/termination', 'POST', { const asmToken = await login(EMAILS.ASM);
dealerId: targetDealer.id, const createRes = await apiRequest('/termination', 'POST', {
category: 'Performance', dealerId: targetDealer.id,
reason: 'Consistently failed to meet commitment targets.', category: args.category || 'Performance',
proposedLwd: new Date().toISOString(), reason: args.reason || 'Consistently failed to meet commitment targets.',
comments: 'Auto termination for testing flow ending with Legal Team, CCO, CEO.' proposedLwd: new Date().toISOString(),
}, asmToken); comments: 'Auto termination for testing flow ending with Legal Team, CCO, CEO.'
}, asmToken);
terminationId = createRes.termination.id;
console.log(`[STEP 1] Termination Request Created. ID: ${terminationId}`);
} else {
console.log(`[STEP 1] Resuming existing termination: ${terminationId}`);
}
const terminationId = createRes.termination.id; const currentTermination = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken);
console.log(`[STEP 1] Termination Request Created. ID: ${terminationId}`); const currentStage = currentTermination?.termination?.currentStage;
console.log(`[INFO] Current stage before progression: ${currentStage}`);
const approvals = [ const approvals = [
{ name: 'RBM Review', email: EMAILS.RBM, remarks: 'Performance concerns validated on-ground. Proceed with termination.' }, { name: 'RBM Review', email: EMAILS.RBM, remarks: 'Performance concerns validated on-ground. Proceed with termination.' },
@ -94,8 +109,17 @@ async function run() {
]; ];
let currentStep = 2; const stageOrder = [
for (const actor of approvals) { 'Submitted', 'RBM Review', 'ZBH Review', 'DD Lead Review', 'Legal Verification', 'DD Head Review',
'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval',
'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'
];
const currentIndex = Math.max(0, stageOrder.indexOf(currentStage));
const currentStepStart = 2 + currentIndex;
let currentStep = currentStepStart;
for (let i = currentIndex; i < approvals.length; i++) {
const actor = approvals[i];
log(currentStep, `${actor.name} (${actor.email}) processing approval...`); log(currentStep, `${actor.name} (${actor.email}) processing approval...`);
const token = await login(actor.email); const token = await login(actor.email);
await apiRequest(`/termination/${terminationId}/status`, 'PUT', { await apiRequest(`/termination/${terminationId}/status`, 'PUT', {
@ -108,13 +132,15 @@ async function run() {
} }
// --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) --- // --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) ---
log(13, 'Starting 16-Department F&F Clearance Flow for Termination...'); if (!SHOULD_SKIP_CLEARANCES) {
log(13, 'Starting 16-Department F&F Clearance Flow for Termination...');
}
const terminationData = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken); const terminationData = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken);
const fnfId = terminationData.termination.fnfSettlement?.id; const fnfId = terminationData.termination.fnfSettlement?.id;
if (!fnfId) { if (!fnfId) {
log('SKIP', 'FnF Settlement not initialized for this termination case.'); log('SKIP', 'FnF Settlement not initialized for this termination case.');
} else { } else if (!SHOULD_SKIP_CLEARANCES) {
const departments = [ const departments = [
{ name: 'Warranty Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'No pending claims.' }, { name: 'Warranty Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'No pending claims.' },
{ name: 'Accessories Department', status: 'Dues', amount: 15000, type: 'Recovery', remarks: 'Shortage in accessory stock.' }, { name: 'Accessories Department', status: 'Dues', amount: 15000, type: 'Recovery', remarks: 'Shortage in accessory stock.' },

View File

@ -4,9 +4,15 @@
*/ */
import fs from 'fs'; import fs from 'fs';
const BASE_URL = 'http://localhost:5000/api'; const args = Object.fromEntries(
process.argv.slice(2)
.map(arg => arg.replace(/^--/, '').split('='))
.map(([k, v]) => [k, v ?? 'true'])
);
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
const PASSWORD = 'Admin@123'; const PASSWORD = 'Admin@123';
const OTP = '123456'; const OTP = '123456';
const STEP_DELAY_MS = Number(args.delayMs || 1000);
// Append timestamp to email to avoid duplicate application error // Append timestamp to email to avoid duplicate application error
const timestamp = Date.now(); const timestamp = Date.now();
@ -79,7 +85,7 @@ let loaRequestId = null;
/** /**
* HELPERS * HELPERS
*/ */
const delay = (ms = 5000) => new Promise(res => setTimeout(res, ms)); const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`); const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
@ -102,8 +108,11 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
} }
async function login(email) { async function login(email) {
if (!login.cache) login.cache = {};
if (login.cache[email]) return login.cache[email];
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD }); const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
return data.token; // Standard login returns token at root login.cache[email] = data.token;
return login.cache[email]; // Standard login returns token at root
} }
async function prospectLogin(phone) { async function prospectLogin(phone) {
@ -131,19 +140,47 @@ async function mockUploadDocument(appId, token, docType) {
return response.json(); return response.json();
} }
async function getApplicationStatus(appId, token) {
const res = await apiRequest(`/onboarding/applications/${appId}`, 'GET', null, token);
return res?.data?.overallStatus || res?.data?.status || 'Unknown';
}
async function ensureMandatoryCodeGenFields(appId, token) {
const details = await apiRequest(`/onboarding/applications/${appId}`, 'GET', null, token);
const app = details?.data || {};
const fallbackFields = {
panNumber: app.panNumber || 'ABCDE1234F',
gstNumber: app.gstNumber || '07ABCDE1234F1Z5',
bankName: app.bankName || 'HDFC Bank',
accountNumber: app.accountNumber || '50100223344556',
ifscCode: app.ifscCode || 'HDFC0001234',
accountHolderName: app.accountHolderName || 'Kumar Automobiles Private Limited',
registeredAddress: app.registeredAddress || '123, Main Road, New Delhi'
};
await apiRequest(`/onboarding/applications/${appId}`, 'PUT', fallbackFields, token);
}
/** /**
* MAIN WORKFLOW * MAIN WORKFLOW
*/ */
async function triggerWorkflow() { async function triggerWorkflow() {
console.log('--- STARTING DEALER ONBOARDING E2E FLOW ---\n'); console.log('--- STARTING DEALER ONBOARDING E2E FLOW ---\n');
// 1. PUBLIC APPLY if (args.applicationId) {
log(1, 'Public Prospect Application Submission...'); applicationId = args.applicationId;
const appResponse = await apiRequest('/onboarding/apply', 'POST', PROSPECT_PAYLOAD); applicationUUID = args.applicationId;
applicationId = appResponse.data.applicationId; log(1, `Resuming with existing application: ${applicationUUID}`);
applicationUUID = appResponse.data.id; } else {
log(1, `Application Created: ${applicationId} (UUID: ${applicationUUID})`); // 1. PUBLIC APPLY
await delay(); log(1, 'Public Prospect Application Submission...');
const appResponse = await apiRequest('/onboarding/apply', 'POST', PROSPECT_PAYLOAD);
applicationId = appResponse.data.applicationId;
applicationUUID = appResponse.data.id;
log(1, `Application Created: ${applicationId} (UUID: ${applicationUUID})`);
await delay();
}
// 2. ADMIN SHORTLIST // 2. ADMIN SHORTLIST
log(2, 'Admin Login & Shortlisting...'); log(2, 'Admin Login & Shortlisting...');
@ -371,14 +408,8 @@ async function triggerWorkflow() {
log(7.5, 'LOI Milestone Complete.'); log(7.5, 'LOI Milestone Complete.');
await delay(); await delay();
// 8. GENERATE DEALER CODES (Sequence: Post-LOI, Pre-LOA) // 8. PAYMENT GATE (SECURITY DEPOSIT FIRST AS PER CURRENT FLOW)
log(8, 'Admin Generating SAP Dealer Codes...'); log(8, 'Finance Verifying SECURITY_DEPOSIT to unlock LOI Issued...');
await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
log(8, 'Dealer Codes Generated.');
await delay();
// 9. PAYMENT GATE
log(9, 'Prospect Uploading Payment Receipt (Mock)...');
const financeToken = await login(EMAILS.FINANCE); const financeToken = await login(EMAILS.FINANCE);
await apiRequest('/loa/security-deposit', 'POST', { await apiRequest('/loa/security-deposit', 'POST', {
applicationId: applicationUUID, applicationId: applicationUUID,
@ -387,9 +418,37 @@ async function triggerWorkflow() {
depositType: 'SECURITY_DEPOSIT', depositType: 'SECURITY_DEPOSIT',
status: 'Verified' status: 'Verified'
}, financeToken); }, financeToken);
log(9, 'Security Deposit Verified.'); log(8, 'Security Deposit Verified.');
await delay();
log(9.1, 'Finance Verifying FIRST FILL (₹15L)...'); // 9. GENERATE DEALER CODES (NOW RETRY-SAFE WITH STATUS CHECK)
let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
log(9, `Current status before code generation: ${statusBeforeCodeGen}`);
log(9, 'Ensuring mandatory PAN/GST/Bank fields before code generation...');
await ensureMandatoryCodeGenFields(applicationUUID, adminToken);
await delay(300);
if (statusBeforeCodeGen === 'Security Details') {
log(9, 'Status is Security Details; re-verifying Security Deposit to move to LOI Issued...');
await apiRequest('/loa/security-deposit', 'POST', {
applicationId: applicationUUID,
amount: 500000,
paymentReference: `PAY-RETRY-${Date.now()}`,
depositType: 'SECURITY_DEPOSIT',
status: 'Verified'
}, financeToken);
await delay();
statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
log(9, `Status after re-verify: ${statusBeforeCodeGen}`);
}
log(9, 'Admin Generating SAP Dealer Codes...');
await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
log(9, 'Dealer Codes Generated.');
await delay();
// 10. FIRST FILL (POST CODE-GENERATION)
log(10, 'Finance Verifying FIRST FILL (₹15L)...');
await apiRequest('/loa/security-deposit', 'POST', { await apiRequest('/loa/security-deposit', 'POST', {
applicationId: applicationUUID, applicationId: applicationUUID,
amount: 1500000, amount: 1500000,
@ -397,11 +456,11 @@ async function triggerWorkflow() {
depositType: 'FIRST_FILL', depositType: 'FIRST_FILL',
status: 'Verified' status: 'Verified'
}, financeToken); }, financeToken);
log(9.1, 'Final Security Deposit Verified.'); log(10, 'Final Security Deposit Verified.');
await delay(); await delay();
// 9.2 ADMIN UPDATING STATUTORY & BANK DETAILS // 11. ADMIN UPDATING STATUTORY & BANK DETAILS
log(9.2, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...'); log(11, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', { await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', {
accountHolderName: 'Ramesh Automobiles Private Limited', accountHolderName: 'Ramesh Automobiles Private Limited',
panNumber: 'ABCDE1234F', panNumber: 'ABCDE1234F',
@ -410,11 +469,11 @@ async function triggerWorkflow() {
accountNumber: '50100223344556', accountNumber: '50100223344556',
ifscCode: 'HDFC0001234' ifscCode: 'HDFC0001234'
}, adminToken); }, adminToken);
log(9.2, 'Statutory & Bank details updated.'); log(11, 'Statutory & Bank details updated.');
await delay(); await delay();
// 10. FINAL LOA APPROVAL // 12. FINAL LOA APPROVAL
log(10, 'NBH & Head Approving Final LOA...'); log(12, 'NBH & Head Approving Final LOA...');
const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken); const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
const finalLoaRequestId = loaRes.data.id; const finalLoaRequestId = loaRes.data.id;
@ -427,16 +486,16 @@ async function triggerWorkflow() {
action: 'Approved', action: 'Approved',
remarks: 'NBH Approval (Level 2)' remarks: 'NBH Approval (Level 2)'
}, nbhToken); }, nbhToken);
log(10, 'LOA Fully Approved.'); log(12, 'LOA Fully Approved.');
await delay(); await delay();
// 11. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION // 13. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
log(11, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...'); log(13, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken); const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken);
const checklistId = eorInit.data.id; const checklistId = eorInit.data.id;
log(11, `EOR Checklist Created (ID: ${checklistId})`); log(13, `EOR Checklist Created (ID: ${checklistId})`);
log(11.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...'); log(13.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...');
const eorItems = [ const eorItems = [
{ itemType: 'Sales', description: 'Sales Standards' }, { itemType: 'Sales', description: 'Sales Standards' },
{ itemType: 'Service', description: 'Service & Spares' }, { itemType: 'Service', description: 'Service & Spares' },
@ -460,9 +519,9 @@ async function triggerWorkflow() {
remarks: 'Verified by Auditor - Compliant' remarks: 'Verified by Auditor - Compliant'
}, adminToken); }, adminToken);
} }
console.log('\n[STEP 11.1] All EOR items marked as compliant.'); console.log('\n[STEP 13.1] All EOR items marked as compliant.');
log(11.2, 'Auditor Submitting Final EOR Audit...'); log(13.2, 'Auditor Submitting Final EOR Audit...');
await apiRequest(`/eor/audit/${checklistId}`, 'POST', { await apiRequest(`/eor/audit/${checklistId}`, 'POST', {
status: 'Completed', status: 'Completed',
overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.' overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
@ -470,32 +529,32 @@ async function triggerWorkflow() {
// Status check // Status check
const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken); const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
log(11.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`); log(13.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
await delay(); await delay();
// 12. FINAL ONBOARDING // 14. FINAL ONBOARDING
log(12, 'Admin Finalizing Dealer Onboarding...'); log(14, 'Admin Finalizing Dealer Onboarding...');
await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken); await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
await delay(); await delay();
// 13. VERIFICATION // 15. VERIFICATION
log(13, 'Verifying Dealer Record Creation...'); log(15, 'Verifying Dealer Record Creation...');
const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken); const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken);
if (!dealerRes.success || !dealerRes.data) { if (!dealerRes.success || !dealerRes.data) {
throw new Error('Verification Failed: Dealer record not found after onboarding.'); throw new Error('Verification Failed: Dealer record not found after onboarding.');
} }
log(13, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`); log(15, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`);
log(13.1, 'Verifying User Account Role Update...'); log(15.1, 'Verifying User Account Role Update...');
const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken); const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken);
const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL); const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL);
if (!dealerUser || dealerUser.roleCode !== 'Dealer') { if (!dealerUser || dealerUser.roleCode !== 'Dealer') {
throw new Error(`Verification Failed: User role not updated to 'Dealer'. Current role: ${dealerUser?.roleCode}`); throw new Error(`Verification Failed: User role not updated to 'Dealer'. Current role: ${dealerUser?.roleCode}`);
} }
log(13.1, `User role confirmed: ${dealerUser.roleCode}`); log(15.1, `User role confirmed: ${dealerUser.roleCode}`);
log(13.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---'); log(15.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
log(13.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`); log(15.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
} }
/** /**