auditlogs enhanced end to end flow checked for the onboarding , cursor used for major file chnages
This commit is contained in:
parent
1d885f9d9f
commit
7e1e43bef3
19
check_loa.js
Normal file
19
check_loa.js
Normal 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
27
check_stuck.js
Normal 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
30
heal_one.js
Normal 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
30
heal_two.js
Normal 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();
|
||||
17
scripts/verify-offboarding-status.ts
Normal file
17
scripts/verify-offboarding-status.ts
Normal 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.');
|
||||
@ -319,6 +319,7 @@ export const AUDIT_ACTIONS = {
|
||||
// FDD
|
||||
FDD_ASSIGNED: 'FDD_ASSIGNED',
|
||||
FDD_REPORT_UPLOADED: 'FDD_REPORT_UPLOADED',
|
||||
FDD_FLAGGED_NON_RESPONSIVE: 'FDD_FLAGGED_NON_RESPONSIVE',
|
||||
|
||||
// LOI & LOA
|
||||
LOI_REQUESTED: 'LOI_REQUESTED',
|
||||
@ -413,6 +414,53 @@ export const DOCUMENT_TYPES = {
|
||||
OTHER: 'Other'
|
||||
} 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
|
||||
export const REQUEST_TYPES = {
|
||||
APPLICATION: 'application',
|
||||
|
||||
@ -42,6 +42,7 @@ export const authenticate = async (req: AuthRequest, res: Response, next: NextFu
|
||||
req.user = {
|
||||
id: application.id,
|
||||
email: application.email,
|
||||
phone: application.phone, // Adding phone here
|
||||
firstName: application.applicantName ? application.applicantName.split(' ')[0] : 'Prospective',
|
||||
lastName: application.applicantName ? application.applicantName.split(' ').slice(1).join(' ') : 'User',
|
||||
fullName: application.applicantName,
|
||||
|
||||
70
src/common/utils/offboardingStatus.ts
Normal file
70
src/common/utils/offboardingStatus.ts
Normal 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';
|
||||
};
|
||||
@ -143,8 +143,8 @@ export const syncApplicationProgress = async (applicationId: string, overallStat
|
||||
// Statuses that imply the CURRENT stage (single or both parallel) is finished
|
||||
const completionStatuses = [
|
||||
'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved',
|
||||
'Level 2 Approved', 'Level 3 Approved', 'LOI Issued',
|
||||
'LOA Issued', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
|
||||
'Level 2 Approved', 'Level 3 Approved',
|
||||
'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
|
||||
];
|
||||
|
||||
const isCurrentStageFinished = completionStatuses.includes(overallStatus);
|
||||
|
||||
@ -38,7 +38,6 @@ import createTerminationAudit from './TerminationAudit.js';
|
||||
import createFnFAudit from './FnFAudit.js';
|
||||
import createConstitutionalAudit from './ConstitutionalAudit.js';
|
||||
import createRelocationAudit from './RelocationAudit.js';
|
||||
import createDealerBankDetail from './DealerBankDetail.js';
|
||||
|
||||
// Batch 1: Organizational Hierarchy & User Management
|
||||
import createRole from './Role.js';
|
||||
@ -67,6 +66,7 @@ import createAiSummary from './AiSummary.js';
|
||||
// Batch 4: Dealer Entity, Documents & Work Notes
|
||||
import createDealer from './Dealer.js';
|
||||
import createDealerCode from './DealerCode.js';
|
||||
import createDealerBankDetail from './DealerBankDetail.js';
|
||||
import createDocumentVersion from './DocumentVersion.js';
|
||||
import createWorkNoteTag from './WorkNoteTag.js';
|
||||
import createWorkNoteAttachment from './WorkNoteAttachment.js';
|
||||
@ -157,7 +157,6 @@ db.TerminationAudit = createTerminationAudit(sequelize);
|
||||
db.FnFAudit = createFnFAudit(sequelize);
|
||||
db.ConstitutionalAudit = createConstitutionalAudit(sequelize);
|
||||
db.RelocationAudit = createRelocationAudit(sequelize);
|
||||
db.DealerBankDetail = createDealerBankDetail(sequelize);
|
||||
|
||||
// Batch 1: Organizational Hierarchy & User Management
|
||||
db.Role = createRole(sequelize);
|
||||
@ -186,6 +185,7 @@ db.AiSummary = createAiSummary(sequelize);
|
||||
// Batch 4: Dealer Entity, Documents & Work Notes
|
||||
db.Dealer = createDealer(sequelize);
|
||||
db.DealerCode = createDealerCode(sequelize);
|
||||
db.DealerBankDetail = createDealerBankDetail(sequelize);
|
||||
db.DocumentVersion = createDocumentVersion(sequelize);
|
||||
db.WorkNoteTag = createWorkNoteTag(sequelize);
|
||||
db.WorkNoteAttachment = createWorkNoteAttachment(sequelize);
|
||||
|
||||
@ -173,9 +173,19 @@ export const getPermissions = async (req: Request, res: Response) => {
|
||||
|
||||
export const getAllUsers = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { roleCode, locationId } = req.query;
|
||||
const { roleCode, locationId, search, page = 1, limit = 100 } = req.query as 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 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,
|
||||
attributes: { exclude: ['password'] },
|
||||
limit: Number(limit),
|
||||
offset: (Number(page) - 1) * Number(limit),
|
||||
include: [
|
||||
{
|
||||
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) => {
|
||||
@ -288,7 +301,7 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
||||
return userJson;
|
||||
});
|
||||
|
||||
res.json({ success: true, data: result });
|
||||
res.json({ success: true, data: result, total: count });
|
||||
} catch (error) {
|
||||
console.error('Get users error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error fetching users' });
|
||||
|
||||
@ -226,21 +226,6 @@ const processStageDecision = async (params: {
|
||||
targetStage = 'Statutory Work';
|
||||
targetProgress = 85;
|
||||
} 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;
|
||||
targetStage = 'LOA';
|
||||
targetProgress = 95;
|
||||
|
||||
@ -63,7 +63,39 @@ const ACTION_DESCRIPTIONS: Record<string, string> = {
|
||||
RESIGNATION_APPROVED: 'Resignation approved',
|
||||
RESIGNATION_REJECTED: 'Resignation rejected',
|
||||
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();
|
||||
|
||||
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({
|
||||
where: { resignationId: entityId as string },
|
||||
where: { resignationId: resolvedResignationId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -103,8 +140,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
count = result.count;
|
||||
logs = result.rows;
|
||||
} 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({
|
||||
where: { terminationRequestId: entityId as string },
|
||||
where: { terminationRequestId: resolvedTerminationId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -112,8 +154,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
count = result.count;
|
||||
logs = result.rows;
|
||||
} 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({
|
||||
where: { fnfId: entityId as string },
|
||||
where: { fnfId: resolvedFnfId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -121,8 +168,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
count = result.count;
|
||||
logs = result.rows;
|
||||
} 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({
|
||||
where: { constitutionalChangeId: entityId as string },
|
||||
where: { constitutionalChangeId: resolvedConstitutionalId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
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
|
||||
const formattedLogs = logs.map((log: any) => {
|
||||
const logData = log.get ? log.get({ plain: true }) : log;
|
||||
const details = logData.details || logData.newData;
|
||||
|
||||
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}`;
|
||||
if (logData.details?.statutoryStatus === 'Flagged') {
|
||||
logData.action = 'FDD_FLAGGED_NON_RESPONSIVE';
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
return getNormalizedAuditPayload(logData, entityType as string, entityId as string);
|
||||
});
|
||||
|
||||
res.json({
|
||||
@ -217,30 +249,50 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
// Dynamic Table Switching
|
||||
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({
|
||||
where: { resignationId: entityId as string },
|
||||
where: { resignationId: resolvedResignationId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} 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({
|
||||
where: { terminationRequestId: entityId as string },
|
||||
where: { terminationRequestId: resolvedTerminationId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} 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({
|
||||
where: { fnfId: entityId as string },
|
||||
where: { fnfId: resolvedFnfId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} 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({
|
||||
where: { constitutionalChangeId: entityId as string },
|
||||
where: { constitutionalChangeId: resolvedConstitutionalId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
@ -214,7 +214,7 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
|
||||
if (checklist) {
|
||||
if (checklist.applicationId) {
|
||||
await db.Application.update({
|
||||
overallStatus: 'Approved',
|
||||
overallStatus: 'Inauguration',
|
||||
progressPercentage: 100
|
||||
}, { where: { id: checklist.applicationId } });
|
||||
|
||||
|
||||
@ -153,3 +153,38 @@ export const uploadReport = async (req: AuthRequest, res: Response) => {
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,5 +8,6 @@ router.use(authenticate as any);
|
||||
router.get('/:applicationId', fddController.getAssignment);
|
||||
router.post('/assign', fddController.assignAgency);
|
||||
router.post('/report', fddController.uploadReport);
|
||||
router.post('/flag', fddController.flagNonResponsive);
|
||||
|
||||
export default router;
|
||||
|
||||
@ -9,6 +9,7 @@ import { AuthRequest } from '../../types/express.types.js';
|
||||
import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js';
|
||||
import { syncLocationManagers } from '../master/syncHierarchy.service.js';
|
||||
import { WorkflowService } from '../../services/WorkflowService.js';
|
||||
import { WorkflowIntegrityService } from '../../services/WorkflowIntegrityService.js';
|
||||
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||
|
||||
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
|
||||
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
|
||||
@ -197,6 +199,9 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
|
||||
where.applicationId = targetId;
|
||||
}
|
||||
|
||||
// PROACTIVE INTEGRITY CHECK: Ensure application isn't stalled before returning
|
||||
await WorkflowIntegrityService.synchronizeApplicationState(targetId);
|
||||
|
||||
const application = await Application.findOne({
|
||||
where,
|
||||
include: [
|
||||
@ -297,7 +302,19 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
// Security Check: Ensure prospective dealer controls data ownership and document privacy
|
||||
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' });
|
||||
}
|
||||
|
||||
@ -852,6 +869,34 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
|
||||
});
|
||||
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
|
||||
const { data: sapData } = await ExternalMocksService.mockGenerateSapCodes(application.applicationId);
|
||||
|
||||
|
||||
@ -17,13 +17,15 @@ export class ProspectiveLoginController {
|
||||
|
||||
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({
|
||||
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) {
|
||||
console.log(`[ProspectiveLogin] Application not found for ${phone}, returning 404`);
|
||||
return res.status(404).json({ message: 'No application found with this phone number' });
|
||||
@ -60,9 +62,13 @@ export class ProspectiveLoginController {
|
||||
}
|
||||
|
||||
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({
|
||||
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) {
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { Response, NextFunction } from 'express';
|
||||
import db from '../../database/models/index.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 { AuthRequest } from '../../types/express.types.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 { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||
import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||
|
||||
// Removed generateResignationId and moved to NomenclatureService
|
||||
|
||||
@ -51,7 +59,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
|
||||
reason,
|
||||
additionalInfo,
|
||||
currentStage: RESIGNATION_STAGES.ASM,
|
||||
status: 'ASM Review',
|
||||
status: getResignationStatusForStage(RESIGNATION_STAGES.ASM),
|
||||
progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM),
|
||||
submittedOn: new Date(),
|
||||
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)
|
||||
export const approveResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
const targetOverride = (req as any).targetStage;
|
||||
@ -237,7 +319,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
||||
// Transition via Workflow Service
|
||||
await ResignationWorkflowService.transitionResignation(resignation, nextStage, req.user.id, {
|
||||
remarks,
|
||||
status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`
|
||||
status: getResignationStatusForStage(nextStage)
|
||||
});
|
||||
|
||||
// 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, {
|
||||
remarks,
|
||||
action: 'Sent Back',
|
||||
status: `${prevStage} Review (Sent Back)`
|
||||
status: `${getResignationStatusForStage(prevStage)} (Sent Back)`
|
||||
});
|
||||
|
||||
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) => {
|
||||
const transaction: Transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
@ -495,16 +677,8 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
||||
// Sync with F&F Clearance if settlement exists
|
||||
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } });
|
||||
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;
|
||||
|
||||
if (numAmount === 0) {
|
||||
fnfStatus = 'NOC Submitted';
|
||||
} else {
|
||||
fnfStatus = 'Dues Pending';
|
||||
}
|
||||
const fnfStatus = normalizeClearanceStatus(status, numAmount);
|
||||
|
||||
const existingClearance = await db.FffClearance.findOne({
|
||||
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;
|
||||
return approveResignation(req, res, next);
|
||||
|
||||
case 'assign':
|
||||
return assignResignation(req, res, next);
|
||||
|
||||
default:
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
|
||||
@ -21,5 +21,6 @@ router.put('/:id/sendback', authenticate as any, resignationController.sendBackR
|
||||
router.post('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
|
||||
|
||||
router.put('/:id/clearance', authenticate as any, uploadSingle, resignationController.updateClearance);
|
||||
router.post('/:id/documents', authenticate as any, uploadSingle, resignationController.uploadResignationDocument);
|
||||
|
||||
export default router;
|
||||
|
||||
@ -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 { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
|
||||
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
|
||||
import { normalizeFnFStatus, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||
|
||||
export const getDepartments = async (req: Request, res: Response) => {
|
||||
try {
|
||||
@ -75,8 +76,9 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
|
||||
const fnf = await FnF.findByPk(id);
|
||||
if (!fnf) return res.status(404).json({ success: false, message: 'F&F settlement not found' });
|
||||
|
||||
const normalizedStatus = normalizeFnFStatus(status || fnf.status);
|
||||
await fnf.update({
|
||||
status: status || fnf.status,
|
||||
status: normalizedStatus,
|
||||
netAmount: finalSettlementAmount || fnf.netAmount,
|
||||
settlementAmount: finalSettlementAmount || fnf.settlementAmount,
|
||||
settlementDate: settlementDate || fnf.settlementDate,
|
||||
@ -91,21 +93,29 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
|
||||
action: AUDIT_ACTIONS.FNF_UPDATED,
|
||||
entityType: 'fnf',
|
||||
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 === 'Completed' || status === FNF_STATUS.COMPLETED) {
|
||||
// If status is being set to Completed, transition parent request via workflow services
|
||||
if (normalizedStatus === FNF_STATUS.COMPLETED) {
|
||||
if (fnf.resignationId) {
|
||||
await Resignation.update(
|
||||
{ status: 'Completed', stage: 'Completed' },
|
||||
{ where: { id: fnf.resignationId } }
|
||||
);
|
||||
const resignation = await Resignation.findByPk(fnf.resignationId);
|
||||
if (resignation) {
|
||||
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) {
|
||||
await TerminationRequest.update(
|
||||
{ status: 'Completed' },
|
||||
{ where: { id: fnf.terminationRequestId } }
|
||||
);
|
||||
const termination = await TerminationRequest.findByPk(fnf.terminationRequestId);
|
||||
if (termination) {
|
||||
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
|
||||
let newStatus = fnf.status;
|
||||
let newStatus = normalizeFnFStatus(fnf.status);
|
||||
if (fnf.status === FNF_STATUS.INITIATED && progressPercentage > 0) {
|
||||
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) => {
|
||||
try {
|
||||
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 } });
|
||||
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({
|
||||
status: status || clearance.status,
|
||||
status: normalizedStatus,
|
||||
remarks: remarks || clearance.remarks,
|
||||
documentId: documentId || clearance.documentId,
|
||||
supportingDocument: supportingDocument || clearance.supportingDocument,
|
||||
supportingDocument: uploadedSupportingDocument || supportingDocument || clearance.supportingDocument,
|
||||
clearedBy: req.user?.id,
|
||||
clearedAt: status === 'Cleared' ? new Date() : clearance.clearedAt
|
||||
clearedAt: normalizedStatus !== 'Pending' ? new Date() : clearance.clearedAt
|
||||
});
|
||||
|
||||
// Automatically update FnF progress
|
||||
@ -373,7 +387,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
|
||||
fnfId: id,
|
||||
action: 'CLEARANCE_UPDATED',
|
||||
remarks: remarks || 'No remarks',
|
||||
details: { department: clearance.department, status }
|
||||
details: { department: clearance.department, status: normalizedStatus }
|
||||
});
|
||||
} catch (auditError) {
|
||||
console.error('[SettlementController] Local FnFAudit creation failed:', auditError);
|
||||
@ -393,7 +407,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
|
||||
[parentKey]: parentId,
|
||||
action: 'STAKEHOLDER_CLEARANCE_UPDATED',
|
||||
remarks: `Automated sync from F&F: ${remarks || 'No remarks'}`,
|
||||
details: { department: clearance.department, status }
|
||||
details: { department: clearance.department, status: normalizedStatus }
|
||||
});
|
||||
} catch (parentAuditError) {
|
||||
console.error('[SettlementController] Parent Audit creation failed:', parentAuditError);
|
||||
|
||||
@ -4,6 +4,7 @@ import * as settlementController from './settlement.controller.js';
|
||||
import { authenticate } from '../../common/middleware/auth.js';
|
||||
import { checkRole } from '../../common/middleware/roleCheck.js';
|
||||
import { ROLES } from '../../common/config/constants.js';
|
||||
import { uploadSingle } from '../../common/middleware/upload.js';
|
||||
|
||||
// All routes require authentication
|
||||
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.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
|
||||
router.post('/fnf/:id/line-items', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.addLineItem);
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import { Response, NextFunction } from 'express';
|
||||
import db from '../../database/models/index.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 { AuthRequest } from '../../types/express.types.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 { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||
import { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||
|
||||
// Create termination request
|
||||
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,
|
||||
initiatedBy: req.user.id,
|
||||
currentStage: TERMINATION_STAGES.SUBMITTED,
|
||||
status: 'Submitted',
|
||||
status: getTerminationStatusForStage(TERMINATION_STAGES.SUBMITTED),
|
||||
progressPercentage: TerminationWorkflowService.calculateProgress(TERMINATION_STAGES.SUBMITTED),
|
||||
timeline: [{
|
||||
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)
|
||||
export const updateTerminationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
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, {
|
||||
remarks,
|
||||
status: nextStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : `${nextStage}`
|
||||
status: getTerminationStatusForStage(nextStage)
|
||||
});
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
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
|
||||
await termination.update({ status: 'Approved (F&F Pending LWD)' }, { transaction });
|
||||
// Keep parent status aligned while waiting for LWD-triggered F&F
|
||||
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');
|
||||
|
||||
const clearances = { ...(termination.departmentalClearances || {}) };
|
||||
const normalizedStatus = normalizeClearanceStatus(status, Number(amount) || 0);
|
||||
clearances[department] = {
|
||||
status,
|
||||
status: normalizedStatus,
|
||||
amount: Number(amount) || 0,
|
||||
type: type || 'Receivable',
|
||||
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 } });
|
||||
if (fnf) {
|
||||
await db.FffClearance.update(
|
||||
{ status, remarks, amount: Number(amount) || 0 },
|
||||
{ status: normalizedStatus, remarks, amount: Number(amount) || 0 },
|
||||
{ where: { fnfId: fnf.id, department }, transaction }
|
||||
);
|
||||
}
|
||||
@ -311,7 +393,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
||||
action: 'CLEARANCE_UPDATED',
|
||||
terminationRequestId: id,
|
||||
remarks: remarks || `Cleared ${department}`,
|
||||
details: { department, status, amount }
|
||||
details: { department, status: normalizedStatus, amount }
|
||||
}, { transaction });
|
||||
|
||||
if (fnf) {
|
||||
@ -320,7 +402,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
||||
fnfId: fnf.id,
|
||||
action: 'CLEARANCE_UPDATED',
|
||||
remarks: remarks || `Departmental clearance recorded for ${department}`,
|
||||
details: { department, status, source: 'Termination Workflow' }
|
||||
details: { department, status: normalizedStatus, source: 'Termination Workflow' }
|
||||
}, { transaction });
|
||||
}
|
||||
|
||||
|
||||
@ -2,9 +2,10 @@ import express from 'express';
|
||||
const router = express.Router();
|
||||
import {
|
||||
createTermination, getTerminations, getTerminationById, updateTerminationStatus,
|
||||
submitScnResponse, recordPersonalHearing, updateClearance
|
||||
submitScnResponse, recordPersonalHearing, updateClearance, uploadTerminationDocument
|
||||
} from './termination.controller.js';
|
||||
import { authenticate } from '../../common/middleware/auth.js';
|
||||
import { uploadSingle } from '../../common/middleware/upload.js';
|
||||
|
||||
router.use(authenticate as any);
|
||||
|
||||
@ -16,5 +17,6 @@ router.post('/:id/status', updateTerminationStatus);
|
||||
router.post('/scn-response', submitScnResponse);
|
||||
router.post('/hearing-record', recordPersonalHearing);
|
||||
router.put('/:id/clearance', updateClearance);
|
||||
router.post('/:id/documents', uploadSingle, uploadTerminationDocument);
|
||||
|
||||
export default router;
|
||||
|
||||
@ -7,11 +7,13 @@ export class ConstitutionalWorkflowService {
|
||||
*/
|
||||
static async transitionRequest(request: any, targetStage: string, userId: string, options: any = {}) {
|
||||
const { action, status, remarks, userFullName } = options;
|
||||
const sourceStage = request.currentStage;
|
||||
|
||||
const updatedTimeline = [
|
||||
...request.timeline,
|
||||
...(request.timeline || []),
|
||||
{
|
||||
stage: targetStage,
|
||||
stage: sourceStage, // Correctly Associate remark with the stage where action happened
|
||||
targetStage: targetStage,
|
||||
timestamp: new Date(),
|
||||
user: userFullName || 'System',
|
||||
action: action || `Moved to ${targetStage}`,
|
||||
@ -35,7 +37,7 @@ export class ConstitutionalWorkflowService {
|
||||
constitutionalChangeId: request.id,
|
||||
action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED,
|
||||
remarks: remarks || '',
|
||||
details: { status: updateData.status, stage: targetStage }
|
||||
details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
|
||||
});
|
||||
|
||||
return request;
|
||||
|
||||
@ -25,13 +25,16 @@ export class RelocationWorkflowService {
|
||||
updateData.progressPercentage = progressPercentage;
|
||||
}
|
||||
|
||||
const sourceStage = request.currentStage;
|
||||
|
||||
// 1. Update Request Record
|
||||
await request.update(updateData);
|
||||
|
||||
// 2. Update Timeline (JSON array)
|
||||
const user = userId ? await User.findByPk(userId) : null;
|
||||
const timelineEntry = {
|
||||
stage: stage || request.currentStage,
|
||||
stage: sourceStage, // Store the stage where the action happened
|
||||
targetStage: stage || targetStatus,
|
||||
timestamp: new Date(),
|
||||
user: user ? user.fullName : 'System',
|
||||
action: action || `Transitioned to ${targetStatus}`,
|
||||
@ -47,7 +50,7 @@ export class RelocationWorkflowService {
|
||||
relocationRequestId: request.id,
|
||||
action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED,
|
||||
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}`);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import db from '../database/models/index.js';
|
||||
const { AuditLog, User, Worknote } = db;
|
||||
import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js';
|
||||
import { getResignationStatusForStage } from '../common/utils/offboardingStatus.js';
|
||||
import { NotificationService } from './NotificationService.js';
|
||||
import { Op } from 'sequelize';
|
||||
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 = {}) {
|
||||
const { action, remarks, status } = metadata;
|
||||
const sourceStage = resignation.currentStage;
|
||||
|
||||
const updateData: any = {
|
||||
currentStage: targetStage,
|
||||
status: status || targetStage,
|
||||
status: status || getResignationStatusForStage(targetStage),
|
||||
progressPercentage: this.calculateProgress(targetStage),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
// 1. Update Resignation Record
|
||||
await resignation.update(updateData);
|
||||
|
||||
// 2. Update Timeline (JSON array)
|
||||
// 2. Update Timeline (JSON array) & Resignation Record
|
||||
const actor = userId ? await User.findByPk(userId) : null;
|
||||
const timelineEntry = {
|
||||
stage: targetStage,
|
||||
stage: sourceStage, // Correctly Associate remark with the stage where action happened
|
||||
targetStage: targetStage, // Store target for reference
|
||||
timestamp: new Date(),
|
||||
user: actor ? actor.fullName : 'System',
|
||||
action: action || `Approved to ${targetStage}`,
|
||||
@ -34,7 +34,11 @@ export class ResignationWorkflowService {
|
||||
};
|
||||
|
||||
const updatedTimeline = [...(resignation.timeline || []), timelineEntry];
|
||||
await resignation.update({ timeline: updatedTimeline });
|
||||
|
||||
await resignation.update({
|
||||
...updateData,
|
||||
timeline: updatedTimeline
|
||||
});
|
||||
|
||||
// 3. Create Audit Log
|
||||
let auditAction: any = AUDIT_ACTIONS.APPROVED;
|
||||
@ -47,7 +51,7 @@ export class ResignationWorkflowService {
|
||||
resignationId: resignation.id,
|
||||
action: auditAction,
|
||||
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
|
||||
|
||||
@ -2,6 +2,7 @@ import db from '../database/models/index.js';
|
||||
import { Op } from 'sequelize';
|
||||
const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db;
|
||||
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 ExternalMocksService from '../common/utils/externalMocks.service.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 = {}) {
|
||||
const { action, remarks, status } = metadata;
|
||||
const sourceStage = termination.currentStage;
|
||||
|
||||
const updateData: any = {
|
||||
currentStage: targetStage,
|
||||
status: status || (targetStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : targetStage),
|
||||
status: status || getTerminationStatusForStage(targetStage),
|
||||
progressPercentage: this.calculateProgress(targetStage),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
// 1. Update Termination Record
|
||||
await termination.update(updateData);
|
||||
|
||||
// 2. Update Timeline (JSON array)
|
||||
// 1. Resolve Actor
|
||||
const actor = userId ? await User.findByPk(userId) : null;
|
||||
|
||||
// 2. Prepare Timeline Entry
|
||||
const timelineEntry = {
|
||||
stage: targetStage,
|
||||
stage: sourceStage, // Correctly Associate remark with the stage where action happened
|
||||
targetStage: targetStage,
|
||||
timestamp: new Date(),
|
||||
user: actor ? actor.fullName : 'System',
|
||||
action: action || `Approved to ${targetStage}`,
|
||||
@ -34,9 +37,14 @@ export class TerminationWorkflowService {
|
||||
};
|
||||
|
||||
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;
|
||||
if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED;
|
||||
if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED;
|
||||
@ -46,7 +54,7 @@ export class TerminationWorkflowService {
|
||||
terminationRequestId: termination.id,
|
||||
action: auditAction,
|
||||
remarks: remarks || '',
|
||||
details: { status: updateData.status, stage: targetStage }
|
||||
details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
|
||||
});
|
||||
|
||||
// 4. Send Notifications
|
||||
|
||||
131
src/services/WorkflowIntegrityService.ts
Normal file
131
src/services/WorkflowIntegrityService.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
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 STEP_DELAY_MS = Number(args.delayMs || 500);
|
||||
|
||||
const EMAILS = {
|
||||
DD_ADMIN: 'lince@gmail.com',
|
||||
@ -45,11 +49,14 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
||||
}
|
||||
|
||||
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 });
|
||||
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() {
|
||||
try {
|
||||
@ -58,17 +65,21 @@ async function run() {
|
||||
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`);
|
||||
const dealerToken = await login(EMAILS.DEALER);
|
||||
|
||||
console.log('[STEP 1] Dealer Submitting Constitutional Change...');
|
||||
const createRes = await apiRequest('/self-service/constitutional', 'POST', {
|
||||
changeType: 'LLP Conversion',
|
||||
reason: 'Converting to LLP for better operational governance.',
|
||||
currentConstitution: 'Proprietorship',
|
||||
newPartnersDetails: 'John Doe, Jane Smith',
|
||||
shareholdingPattern: '60/40'
|
||||
}, dealerToken);
|
||||
|
||||
const requestId = createRes.requestId;
|
||||
console.log(`[STEP 1] Request Created. RequestID: ${requestId}`);
|
||||
let requestId = args.requestId;
|
||||
if (!requestId) {
|
||||
console.log('[STEP 1] Dealer Submitting Constitutional Change...');
|
||||
const createRes = await apiRequest('/self-service/constitutional', 'POST', {
|
||||
changeType: args.changeType || 'LLP Conversion',
|
||||
reason: args.reason || 'Converting to LLP for better operational governance.',
|
||||
currentConstitution: 'Proprietorship',
|
||||
newPartnersDetails: 'John Doe, Jane Smith',
|
||||
shareholdingPattern: '60/40'
|
||||
}, dealerToken);
|
||||
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
|
||||
const approvalSequence = [
|
||||
@ -82,8 +93,15 @@ async function run() {
|
||||
{ name: 'Legal Finalize', email: EMAILS.LEGAL }
|
||||
];
|
||||
|
||||
let currentStep = 2;
|
||||
for (const actor of approvalSequence) {
|
||||
const adminToken = await login(EMAILS.DD_ADMIN);
|
||||
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...`);
|
||||
const token = await login(actor.email);
|
||||
const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', {
|
||||
@ -96,7 +114,6 @@ async function run() {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') {
|
||||
|
||||
156
trigger-relocation.js
Normal file
156
trigger-relocation.js
Normal 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();
|
||||
@ -1,7 +1,13 @@
|
||||
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 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 = {
|
||||
DD_ADMIN: 'lince@gmail.com',
|
||||
@ -44,16 +50,19 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
||||
}
|
||||
|
||||
async function login(email) {
|
||||
if (!login.cache) login.cache = {};
|
||||
if (login.cache[email]) return login.cache[email];
|
||||
const isInternal = email.endsWith('@royalenfield.com') ||
|
||||
email === 'lince@gmail.com' ||
|
||||
email === 'yashwin@gmail.com';
|
||||
const password = isInternal ? 'Admin@123' : 'Dealer@123';
|
||||
|
||||
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}`);
|
||||
|
||||
async function run() {
|
||||
@ -101,29 +110,32 @@ async function run() {
|
||||
console.log(`Found Target Outlet: ${targetOutlet.name} (${targetOutlet.code})`);
|
||||
|
||||
console.log(`[STEP 1.2] Dealer Submitting Resignation for Outlet...`);
|
||||
let resignationId;
|
||||
try {
|
||||
const createRes = await apiRequest('/self-service/resignations', 'POST', {
|
||||
outletId: targetOutlet.id,
|
||||
resignationType: 'Voluntary',
|
||||
lastOperationalDateSales: new Date().toISOString().split('T')[0],
|
||||
lastOperationalDateServices: new Date().toISOString().split('T')[0],
|
||||
reason: 'Focusing on other business ventures',
|
||||
remarks: 'Initiating voluntary resignation for E2E validation.'
|
||||
}, dealerToken);
|
||||
resignationId = createRes.resignation.id;
|
||||
log(1, `Resignation Created. ID: ${resignationId}`);
|
||||
} catch (e) {
|
||||
if (e.message.includes('already has an active resignation request')) {
|
||||
console.log(`[STEP 1.2] Active resignation already exists. Fetching...`);
|
||||
// Use plural route for listing
|
||||
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));
|
||||
resignationId = activeRes.id;
|
||||
log(1, `Resuming with existig Resignation: ${resignationId}`);
|
||||
} else {
|
||||
throw e;
|
||||
let resignationId = args.resignationId;
|
||||
if (!resignationId) {
|
||||
try {
|
||||
const createRes = await apiRequest('/self-service/resignations', 'POST', {
|
||||
outletId: targetOutlet.id,
|
||||
resignationType: 'Voluntary',
|
||||
lastOperationalDateSales: new Date().toISOString().split('T')[0],
|
||||
lastOperationalDateServices: new Date().toISOString().split('T')[0],
|
||||
reason: 'Focusing on other business ventures',
|
||||
remarks: 'Initiating voluntary resignation for E2E validation.'
|
||||
}, dealerToken);
|
||||
resignationId = createRes.resignation.id;
|
||||
log(1, `Resignation Created. ID: ${resignationId}`);
|
||||
} catch (e) {
|
||||
if (e.message.includes('already has an active resignation request')) {
|
||||
console.log(`[STEP 1.2] Active resignation already exists. Fetching...`);
|
||||
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));
|
||||
resignationId = activeRes.id;
|
||||
log(1, `Resuming with existing Resignation: ${resignationId}`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log(1, `Resuming provided resignation: ${resignationId}`);
|
||||
}
|
||||
|
||||
await delay();
|
||||
@ -165,7 +177,9 @@ async function run() {
|
||||
}
|
||||
|
||||
// --- 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
|
||||
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.' }
|
||||
];
|
||||
|
||||
for (const dept of departments) {
|
||||
log('9.1', `Clearing Dept: ${dept.name} [${dept.status}] - ${dept.remarks}`);
|
||||
await apiRequest(`/self-service/resignations/${resignationId}/clearance`, 'PUT', {
|
||||
department: dept.name,
|
||||
status: dept.status,
|
||||
remarks: dept.remarks,
|
||||
amount: dept.amount,
|
||||
type: dept.type
|
||||
}, adminToken);
|
||||
await delay(100);
|
||||
if (!SHOULD_SKIP_CLEARANCES) {
|
||||
for (const dept of departments) {
|
||||
log('9.1', `Clearing Dept: ${dept.name} [${dept.status}] - ${dept.remarks}`);
|
||||
await apiRequest(`/self-service/resignations/${resignationId}/clearance`, 'PUT', {
|
||||
department: dept.name,
|
||||
status: dept.status,
|
||||
remarks: dept.remarks,
|
||||
amount: dept.amount,
|
||||
type: dept.type
|
||||
}, adminToken);
|
||||
await delay(100);
|
||||
}
|
||||
log(9, 'All 16 Departments Cleared.');
|
||||
await delay();
|
||||
}
|
||||
log(9, 'All 16 Departments Cleared.');
|
||||
await delay();
|
||||
|
||||
// --- FINAL FINANCE SETTLEMENT ---
|
||||
console.log('[STEP 10] Finance Finalizing Settlement...');
|
||||
const financeToken = await login(EMAILS.FINANCE);
|
||||
await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', {
|
||||
status: 'Completed',
|
||||
finalSettlementAmount: 415173, // Matches your observed amount
|
||||
paymentMode: 'NEFT / Bank Transfer',
|
||||
transactionReference: `TXN-${Date.now()}`,
|
||||
settlementDate: new Date().toISOString(),
|
||||
remarks: 'Settlement completed and verified via automated script.'
|
||||
}, financeToken);
|
||||
await delay();
|
||||
if (!SHOULD_SKIP_FINAL_SETTLEMENT) {
|
||||
console.log('[STEP 10] Finance Finalizing Settlement...');
|
||||
const financeToken = await login(EMAILS.FINANCE);
|
||||
await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', {
|
||||
status: 'Completed',
|
||||
finalSettlementAmount: Number(args.finalSettlementAmount || 415173),
|
||||
paymentMode: 'NEFT / Bank Transfer',
|
||||
transactionReference: `TXN-${Date.now()}`,
|
||||
settlementDate: new Date().toISOString(),
|
||||
remarks: 'Settlement completed and verified via automated script.'
|
||||
}, financeToken);
|
||||
await delay();
|
||||
}
|
||||
|
||||
// --- FINAL COMPLETION ---
|
||||
console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...');
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
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 STEP_DELAY_MS = Number(args.delayMs || 500);
|
||||
const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true';
|
||||
|
||||
const EMAILS = {
|
||||
DD_ADMIN: 'lince@gmail.com',
|
||||
@ -44,11 +49,14 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
||||
}
|
||||
|
||||
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 });
|
||||
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}`);
|
||||
|
||||
async function run() {
|
||||
@ -63,19 +71,26 @@ async function run() {
|
||||
|
||||
console.log(`Targeting Dealer: ${targetDealer.legalName} (${targetDealer.id})`);
|
||||
|
||||
// STEP 1: Submission (ASM)
|
||||
console.log('[STEP 1] ASM Initiating Termination...');
|
||||
const asmToken = await login(EMAILS.ASM);
|
||||
const createRes = await apiRequest('/termination', 'POST', {
|
||||
dealerId: targetDealer.id,
|
||||
category: 'Performance',
|
||||
reason: 'Consistently failed to meet commitment targets.',
|
||||
proposedLwd: new Date().toISOString(),
|
||||
comments: 'Auto termination for testing flow ending with Legal Team, CCO, CEO.'
|
||||
}, asmToken);
|
||||
let terminationId = args.terminationId;
|
||||
if (!terminationId) {
|
||||
console.log('[STEP 1] ASM Initiating Termination...');
|
||||
const asmToken = await login(EMAILS.ASM);
|
||||
const createRes = await apiRequest('/termination', 'POST', {
|
||||
dealerId: targetDealer.id,
|
||||
category: args.category || 'Performance',
|
||||
reason: args.reason || 'Consistently failed to meet commitment targets.',
|
||||
proposedLwd: new Date().toISOString(),
|
||||
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;
|
||||
console.log(`[STEP 1] Termination Request Created. ID: ${terminationId}`);
|
||||
const currentTermination = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken);
|
||||
const currentStage = currentTermination?.termination?.currentStage;
|
||||
console.log(`[INFO] Current stage before progression: ${currentStage}`);
|
||||
|
||||
const approvals = [
|
||||
{ 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;
|
||||
for (const actor of approvals) {
|
||||
const stageOrder = [
|
||||
'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...`);
|
||||
const token = await login(actor.email);
|
||||
await apiRequest(`/termination/${terminationId}/status`, 'PUT', {
|
||||
@ -108,13 +132,15 @@ async function run() {
|
||||
}
|
||||
|
||||
// --- 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 fnfId = terminationData.termination.fnfSettlement?.id;
|
||||
|
||||
if (!fnfId) {
|
||||
log('SKIP', 'FnF Settlement not initialized for this termination case.');
|
||||
} else {
|
||||
} else if (!SHOULD_SKIP_CLEARANCES) {
|
||||
const departments = [
|
||||
{ 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.' },
|
||||
|
||||
@ -4,9 +4,15 @@
|
||||
*/
|
||||
|
||||
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 OTP = '123456';
|
||||
const STEP_DELAY_MS = Number(args.delayMs || 1000);
|
||||
|
||||
// Append timestamp to email to avoid duplicate application error
|
||||
const timestamp = Date.now();
|
||||
@ -79,7 +85,7 @@ let loaRequestId = null;
|
||||
/**
|
||||
* 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}`);
|
||||
|
||||
@ -102,8 +108,11 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
||||
}
|
||||
|
||||
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 });
|
||||
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) {
|
||||
@ -131,19 +140,47 @@ async function mockUploadDocument(appId, token, docType) {
|
||||
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
|
||||
*/
|
||||
async function triggerWorkflow() {
|
||||
console.log('--- STARTING DEALER ONBOARDING E2E FLOW ---\n');
|
||||
|
||||
// 1. PUBLIC APPLY
|
||||
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();
|
||||
if (args.applicationId) {
|
||||
applicationId = args.applicationId;
|
||||
applicationUUID = args.applicationId;
|
||||
log(1, `Resuming with existing application: ${applicationUUID}`);
|
||||
} else {
|
||||
// 1. PUBLIC APPLY
|
||||
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
|
||||
log(2, 'Admin Login & Shortlisting...');
|
||||
@ -371,14 +408,8 @@ async function triggerWorkflow() {
|
||||
log(7.5, 'LOI Milestone Complete.');
|
||||
await delay();
|
||||
|
||||
// 8. GENERATE DEALER CODES (Sequence: Post-LOI, Pre-LOA)
|
||||
log(8, 'Admin Generating SAP Dealer Codes...');
|
||||
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)...');
|
||||
// 8. PAYMENT GATE (SECURITY DEPOSIT FIRST AS PER CURRENT FLOW)
|
||||
log(8, 'Finance Verifying SECURITY_DEPOSIT to unlock LOI Issued...');
|
||||
const financeToken = await login(EMAILS.FINANCE);
|
||||
await apiRequest('/loa/security-deposit', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
@ -387,9 +418,37 @@ async function triggerWorkflow() {
|
||||
depositType: 'SECURITY_DEPOSIT',
|
||||
status: 'Verified'
|
||||
}, 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', {
|
||||
applicationId: applicationUUID,
|
||||
amount: 1500000,
|
||||
@ -397,11 +456,11 @@ async function triggerWorkflow() {
|
||||
depositType: 'FIRST_FILL',
|
||||
status: 'Verified'
|
||||
}, financeToken);
|
||||
log(9.1, 'Final Security Deposit Verified.');
|
||||
log(10, 'Final Security Deposit Verified.');
|
||||
await delay();
|
||||
|
||||
// 9.2 ADMIN UPDATING STATUTORY & BANK DETAILS
|
||||
log(9.2, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
|
||||
// 11. ADMIN UPDATING STATUTORY & BANK DETAILS
|
||||
log(11, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
|
||||
await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', {
|
||||
accountHolderName: 'Ramesh Automobiles Private Limited',
|
||||
panNumber: 'ABCDE1234F',
|
||||
@ -410,11 +469,11 @@ async function triggerWorkflow() {
|
||||
accountNumber: '50100223344556',
|
||||
ifscCode: 'HDFC0001234'
|
||||
}, adminToken);
|
||||
log(9.2, 'Statutory & Bank details updated.');
|
||||
log(11, 'Statutory & Bank details updated.');
|
||||
await delay();
|
||||
|
||||
// 10. FINAL LOA APPROVAL
|
||||
log(10, 'NBH & Head Approving Final LOA...');
|
||||
// 12. FINAL LOA APPROVAL
|
||||
log(12, 'NBH & Head Approving Final LOA...');
|
||||
const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
|
||||
const finalLoaRequestId = loaRes.data.id;
|
||||
|
||||
@ -427,16 +486,16 @@ async function triggerWorkflow() {
|
||||
action: 'Approved',
|
||||
remarks: 'NBH Approval (Level 2)'
|
||||
}, nbhToken);
|
||||
log(10, 'LOA Fully Approved.');
|
||||
log(12, 'LOA Fully Approved.');
|
||||
await delay();
|
||||
|
||||
// 11. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
|
||||
log(11, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
|
||||
// 13. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
|
||||
log(13, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
|
||||
const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
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 = [
|
||||
{ itemType: 'Sales', description: 'Sales Standards' },
|
||||
{ itemType: 'Service', description: 'Service & Spares' },
|
||||
@ -460,9 +519,9 @@ async function triggerWorkflow() {
|
||||
remarks: 'Verified by Auditor - Compliant'
|
||||
}, 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', {
|
||||
status: 'Completed',
|
||||
overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
|
||||
@ -470,32 +529,32 @@ async function triggerWorkflow() {
|
||||
|
||||
// Status check
|
||||
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();
|
||||
|
||||
// 12. FINAL ONBOARDING
|
||||
log(12, 'Admin Finalizing Dealer Onboarding...');
|
||||
// 14. FINAL ONBOARDING
|
||||
log(14, 'Admin Finalizing Dealer Onboarding...');
|
||||
await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
await delay();
|
||||
|
||||
// 13. VERIFICATION
|
||||
log(13, 'Verifying Dealer Record Creation...');
|
||||
// 15. VERIFICATION
|
||||
log(15, 'Verifying Dealer Record Creation...');
|
||||
const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken);
|
||||
if (!dealerRes.success || !dealerRes.data) {
|
||||
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 dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL);
|
||||
if (!dealerUser || dealerUser.roleCode !== 'Dealer') {
|
||||
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(13.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
|
||||
log(15.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
|
||||
log(15.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user