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
|
||||||
FDD_ASSIGNED: 'FDD_ASSIGNED',
|
FDD_ASSIGNED: 'FDD_ASSIGNED',
|
||||||
FDD_REPORT_UPLOADED: 'FDD_REPORT_UPLOADED',
|
FDD_REPORT_UPLOADED: 'FDD_REPORT_UPLOADED',
|
||||||
|
FDD_FLAGGED_NON_RESPONSIVE: 'FDD_FLAGGED_NON_RESPONSIVE',
|
||||||
|
|
||||||
// LOI & LOA
|
// LOI & LOA
|
||||||
LOI_REQUESTED: 'LOI_REQUESTED',
|
LOI_REQUESTED: 'LOI_REQUESTED',
|
||||||
@ -413,6 +414,53 @@ export const DOCUMENT_TYPES = {
|
|||||||
OTHER: 'Other'
|
OTHER: 'Other'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const RESIGNATION_DOCUMENT_TYPES = [
|
||||||
|
'Resignation Letter',
|
||||||
|
'Dealer Undertaking',
|
||||||
|
'Approval Note',
|
||||||
|
'Legal Communication',
|
||||||
|
'Handover Document',
|
||||||
|
'Settlement Supporting Document',
|
||||||
|
'Other'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const RESIGNATION_DOCUMENT_STAGES = [
|
||||||
|
'ASM',
|
||||||
|
'RBM',
|
||||||
|
'ZBH',
|
||||||
|
'DD Lead',
|
||||||
|
'NBH',
|
||||||
|
'DD Admin',
|
||||||
|
'Legal',
|
||||||
|
'F&F Initiated'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const TERMINATION_DOCUMENT_TYPES = [
|
||||||
|
'Termination Recommendation',
|
||||||
|
'Show Cause Notice',
|
||||||
|
'SCN Response',
|
||||||
|
'Hearing Record',
|
||||||
|
'Approval Note',
|
||||||
|
'Termination Letter',
|
||||||
|
'Settlement Supporting Document',
|
||||||
|
'Other'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const TERMINATION_DOCUMENT_STAGES = [
|
||||||
|
'Submitted',
|
||||||
|
'RBM Review',
|
||||||
|
'ZBH Review',
|
||||||
|
'DD Lead Review',
|
||||||
|
'Legal Verification',
|
||||||
|
'NBH Evaluation',
|
||||||
|
'Show Cause Notice',
|
||||||
|
'Personal Hearing',
|
||||||
|
'NBH Final Approval',
|
||||||
|
'CCO Approval',
|
||||||
|
'CEO Final Approval',
|
||||||
|
'Legal - Termination Letter'
|
||||||
|
] as const;
|
||||||
|
|
||||||
// Request Types
|
// Request Types
|
||||||
export const REQUEST_TYPES = {
|
export const REQUEST_TYPES = {
|
||||||
APPLICATION: 'application',
|
APPLICATION: 'application',
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export const authenticate = async (req: AuthRequest, res: Response, next: NextFu
|
|||||||
req.user = {
|
req.user = {
|
||||||
id: application.id,
|
id: application.id,
|
||||||
email: application.email,
|
email: application.email,
|
||||||
|
phone: application.phone, // Adding phone here
|
||||||
firstName: application.applicantName ? application.applicantName.split(' ')[0] : 'Prospective',
|
firstName: application.applicantName ? application.applicantName.split(' ')[0] : 'Prospective',
|
||||||
lastName: application.applicantName ? application.applicantName.split(' ').slice(1).join(' ') : 'User',
|
lastName: application.applicantName ? application.applicantName.split(' ').slice(1).join(' ') : 'User',
|
||||||
fullName: application.applicantName,
|
fullName: application.applicantName,
|
||||||
|
|||||||
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
|
// Statuses that imply the CURRENT stage (single or both parallel) is finished
|
||||||
const completionStatuses = [
|
const completionStatuses = [
|
||||||
'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved',
|
'Submitted', 'Questionnaire Completed', 'Shortlisted', 'Level 1 Approved',
|
||||||
'Level 2 Approved', 'Level 3 Approved', 'LOI Issued',
|
'Level 2 Approved', 'Level 3 Approved',
|
||||||
'LOA Issued', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
|
'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'
|
||||||
];
|
];
|
||||||
|
|
||||||
const isCurrentStageFinished = completionStatuses.includes(overallStatus);
|
const isCurrentStageFinished = completionStatuses.includes(overallStatus);
|
||||||
|
|||||||
@ -38,7 +38,6 @@ import createTerminationAudit from './TerminationAudit.js';
|
|||||||
import createFnFAudit from './FnFAudit.js';
|
import createFnFAudit from './FnFAudit.js';
|
||||||
import createConstitutionalAudit from './ConstitutionalAudit.js';
|
import createConstitutionalAudit from './ConstitutionalAudit.js';
|
||||||
import createRelocationAudit from './RelocationAudit.js';
|
import createRelocationAudit from './RelocationAudit.js';
|
||||||
import createDealerBankDetail from './DealerBankDetail.js';
|
|
||||||
|
|
||||||
// Batch 1: Organizational Hierarchy & User Management
|
// Batch 1: Organizational Hierarchy & User Management
|
||||||
import createRole from './Role.js';
|
import createRole from './Role.js';
|
||||||
@ -67,6 +66,7 @@ import createAiSummary from './AiSummary.js';
|
|||||||
// Batch 4: Dealer Entity, Documents & Work Notes
|
// Batch 4: Dealer Entity, Documents & Work Notes
|
||||||
import createDealer from './Dealer.js';
|
import createDealer from './Dealer.js';
|
||||||
import createDealerCode from './DealerCode.js';
|
import createDealerCode from './DealerCode.js';
|
||||||
|
import createDealerBankDetail from './DealerBankDetail.js';
|
||||||
import createDocumentVersion from './DocumentVersion.js';
|
import createDocumentVersion from './DocumentVersion.js';
|
||||||
import createWorkNoteTag from './WorkNoteTag.js';
|
import createWorkNoteTag from './WorkNoteTag.js';
|
||||||
import createWorkNoteAttachment from './WorkNoteAttachment.js';
|
import createWorkNoteAttachment from './WorkNoteAttachment.js';
|
||||||
@ -157,7 +157,6 @@ db.TerminationAudit = createTerminationAudit(sequelize);
|
|||||||
db.FnFAudit = createFnFAudit(sequelize);
|
db.FnFAudit = createFnFAudit(sequelize);
|
||||||
db.ConstitutionalAudit = createConstitutionalAudit(sequelize);
|
db.ConstitutionalAudit = createConstitutionalAudit(sequelize);
|
||||||
db.RelocationAudit = createRelocationAudit(sequelize);
|
db.RelocationAudit = createRelocationAudit(sequelize);
|
||||||
db.DealerBankDetail = createDealerBankDetail(sequelize);
|
|
||||||
|
|
||||||
// Batch 1: Organizational Hierarchy & User Management
|
// Batch 1: Organizational Hierarchy & User Management
|
||||||
db.Role = createRole(sequelize);
|
db.Role = createRole(sequelize);
|
||||||
@ -186,6 +185,7 @@ db.AiSummary = createAiSummary(sequelize);
|
|||||||
// Batch 4: Dealer Entity, Documents & Work Notes
|
// Batch 4: Dealer Entity, Documents & Work Notes
|
||||||
db.Dealer = createDealer(sequelize);
|
db.Dealer = createDealer(sequelize);
|
||||||
db.DealerCode = createDealerCode(sequelize);
|
db.DealerCode = createDealerCode(sequelize);
|
||||||
|
db.DealerBankDetail = createDealerBankDetail(sequelize);
|
||||||
db.DocumentVersion = createDocumentVersion(sequelize);
|
db.DocumentVersion = createDocumentVersion(sequelize);
|
||||||
db.WorkNoteTag = createWorkNoteTag(sequelize);
|
db.WorkNoteTag = createWorkNoteTag(sequelize);
|
||||||
db.WorkNoteAttachment = createWorkNoteAttachment(sequelize);
|
db.WorkNoteAttachment = createWorkNoteAttachment(sequelize);
|
||||||
|
|||||||
@ -173,9 +173,19 @@ export const getPermissions = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const getAllUsers = async (req: Request, res: Response) => {
|
export const getAllUsers = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { roleCode, locationId } = req.query;
|
const { roleCode, locationId, search, page = 1, limit = 100 } = req.query as any;
|
||||||
const whereClause: any = {};
|
const whereClause: any = {};
|
||||||
|
|
||||||
|
// 1. Search filter
|
||||||
|
if (search) {
|
||||||
|
whereClause[Op.or] = [
|
||||||
|
{ fullName: { [Op.iLike]: `%${search}%` } },
|
||||||
|
{ email: { [Op.iLike]: `%${search}%` } },
|
||||||
|
{ employeeId: { [Op.iLike]: `%${search}%` } }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Role filter
|
||||||
let rawRoleCode: any = roleCode || req.query['roleCode[]'];
|
let rawRoleCode: any = roleCode || req.query['roleCode[]'];
|
||||||
let finalRoleCodes: string[] = [];
|
let finalRoleCodes: string[] = [];
|
||||||
|
|
||||||
@ -213,9 +223,11 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await User.findAll({
|
const { count, rows: users } = await User.findAndCountAll({
|
||||||
where: whereClause,
|
where: whereClause,
|
||||||
attributes: { exclude: ['password'] },
|
attributes: { exclude: ['password'] },
|
||||||
|
limit: Number(limit),
|
||||||
|
offset: (Number(page) - 1) * Number(limit),
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Role,
|
model: Role,
|
||||||
@ -248,7 +260,8 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']],
|
||||||
|
distinct: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = users.map((u: any) => {
|
const result = users.map((u: any) => {
|
||||||
@ -288,7 +301,7 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
|||||||
return userJson;
|
return userJson;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, data: result });
|
res.json({ success: true, data: result, total: count });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get users error:', error);
|
console.error('Get users error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error fetching users' });
|
res.status(500).json({ success: false, message: 'Error fetching users' });
|
||||||
|
|||||||
@ -226,21 +226,6 @@ const processStageDecision = async (params: {
|
|||||||
targetStage = 'Statutory Work';
|
targetStage = 'Statutory Work';
|
||||||
targetProgress = 85;
|
targetProgress = 85;
|
||||||
} else if (stageCode === 'LOA_APPROVAL') {
|
} else if (stageCode === 'LOA_APPROVAL') {
|
||||||
// Hard-stop validation: Check for statutory and bank details
|
|
||||||
const missingFields = [];
|
|
||||||
if (!application.panNumber) missingFields.push('PAN Number');
|
|
||||||
if (!application.gstNumber) missingFields.push('GST Number');
|
|
||||||
if (!application.bankName) missingFields.push('Bank Name');
|
|
||||||
if (!application.accountNumber) missingFields.push('Account Number');
|
|
||||||
if (!application.ifscCode) missingFields.push('IFSC Code');
|
|
||||||
|
|
||||||
if (decision === 'Approved' && missingFields.length > 0) {
|
|
||||||
return {
|
|
||||||
forbidden: true,
|
|
||||||
message: `Cannot approve LOA: Missing mandatory fields: ${missingFields.join(', ')}. Please ensure they are filled in the application details.`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
targetStatus = APPLICATION_STATUS.LOA_ISSUED;
|
targetStatus = APPLICATION_STATUS.LOA_ISSUED;
|
||||||
targetStage = 'LOA';
|
targetStage = 'LOA';
|
||||||
targetProgress = 95;
|
targetProgress = 95;
|
||||||
|
|||||||
@ -63,7 +63,39 @@ const ACTION_DESCRIPTIONS: Record<string, string> = {
|
|||||||
RESIGNATION_APPROVED: 'Resignation approved',
|
RESIGNATION_APPROVED: 'Resignation approved',
|
||||||
RESIGNATION_REJECTED: 'Resignation rejected',
|
RESIGNATION_REJECTED: 'Resignation rejected',
|
||||||
EMAIL_SENT: 'Email notification sent',
|
EMAIL_SENT: 'Email notification sent',
|
||||||
REMINDER_SENT: 'Reminder sent'
|
REMINDER_SENT: 'Reminder sent',
|
||||||
|
FDD_FLAGGED_NON_RESPONSIVE: 'APPLICANT FLAGGED: Non-responsive to audit queries'
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNormalizedAuditPayload = (logData: any, entityType: string, entityId: string) => {
|
||||||
|
const payload = logData.details || logData.newData || {};
|
||||||
|
const actorName = logData.user?.fullName || logData.userName || 'System';
|
||||||
|
const action = logData.action || 'UPDATED';
|
||||||
|
|
||||||
|
let description = ACTION_DESCRIPTIONS[action] ||
|
||||||
|
String(action).split('_').map((w: any) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
|
||||||
|
|
||||||
|
if (payload?.stage) description += ` - Stage: ${payload.stage}`;
|
||||||
|
else if (payload?.department) description += ` - ${payload.department}`;
|
||||||
|
else if (payload?.status && action === 'UPDATED') description += ` to ${payload.status}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: logData.id,
|
||||||
|
action,
|
||||||
|
description,
|
||||||
|
entityType,
|
||||||
|
entityId,
|
||||||
|
actor: {
|
||||||
|
name: actorName,
|
||||||
|
email: logData.user?.email || logData.userEmail || null
|
||||||
|
},
|
||||||
|
userName: actorName,
|
||||||
|
userEmail: logData.user?.email || logData.userEmail || null,
|
||||||
|
remarks: logData.remarks || payload?.remarks || '',
|
||||||
|
newData: payload,
|
||||||
|
details: payload,
|
||||||
|
timestamp: logData.createdAt || logData.timestamp
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,8 +126,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
const type = (entityType as string).toLowerCase();
|
const type = (entityType as string).toLowerCase();
|
||||||
|
|
||||||
if (type === 'resignation') {
|
if (type === 'resignation') {
|
||||||
|
const resignation = await db.Resignation.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedResignationId = resignation?.id || (entityId as string);
|
||||||
const result = await db.ResignationAudit.findAndCountAll({
|
const result = await db.ResignationAudit.findAndCountAll({
|
||||||
where: { resignationId: entityId as string },
|
where: { resignationId: resolvedResignationId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -103,8 +140,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
count = result.count;
|
count = result.count;
|
||||||
logs = result.rows;
|
logs = result.rows;
|
||||||
} else if (type === 'termination') {
|
} else if (type === 'termination') {
|
||||||
|
const termination = await db.TerminationRequest.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedTerminationId = termination?.id || (entityId as string);
|
||||||
const result = await db.TerminationAudit.findAndCountAll({
|
const result = await db.TerminationAudit.findAndCountAll({
|
||||||
where: { terminationRequestId: entityId as string },
|
where: { terminationRequestId: resolvedTerminationId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -112,8 +154,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
count = result.count;
|
count = result.count;
|
||||||
logs = result.rows;
|
logs = result.rows;
|
||||||
} else if (type === 'fnf') {
|
} else if (type === 'fnf') {
|
||||||
|
const fnf = await db.FnF.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedFnfId = fnf?.id || (entityId as string);
|
||||||
const result = await db.FnFAudit.findAndCountAll({
|
const result = await db.FnFAudit.findAndCountAll({
|
||||||
where: { fnfId: entityId as string },
|
where: { fnfId: resolvedFnfId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -121,8 +168,13 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
count = result.count;
|
count = result.count;
|
||||||
logs = result.rows;
|
logs = result.rows;
|
||||||
} else if (type === 'constitutional_change') {
|
} else if (type === 'constitutional_change') {
|
||||||
|
const constitutional = await db.ConstitutionalChange.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
|
||||||
const result = await db.ConstitutionalAudit.findAndCountAll({
|
const result = await db.ConstitutionalAudit.findAndCountAll({
|
||||||
where: { constitutionalChangeId: entityId as string },
|
where: { constitutionalChangeId: resolvedConstitutionalId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -155,30 +207,10 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
// Format the response with human-readable descriptions and consistent mapping
|
// Format the response with human-readable descriptions and consistent mapping
|
||||||
const formattedLogs = logs.map((log: any) => {
|
const formattedLogs = logs.map((log: any) => {
|
||||||
const logData = log.get ? log.get({ plain: true }) : log;
|
const logData = log.get ? log.get({ plain: true }) : log;
|
||||||
const details = logData.details || logData.newData;
|
if (logData.details?.statutoryStatus === 'Flagged') {
|
||||||
|
logData.action = 'FDD_FLAGGED_NON_RESPONSIVE';
|
||||||
let baseDescription = ACTION_DESCRIPTIONS[logData.action] ||
|
|
||||||
logData.action.split('_').map((w: any) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
|
|
||||||
|
|
||||||
// EXCLUSIVE ADDITION: Enhance description with context if available (Stage/Status/Dept)
|
|
||||||
if (details) {
|
|
||||||
if (details.stage) baseDescription += ` - Stage: ${details.stage}`;
|
|
||||||
else if (details.department) baseDescription += ` - ${details.department}`;
|
|
||||||
else if (details.status && logData.action === 'UPDATED') baseDescription += ` to ${details.status}`;
|
|
||||||
}
|
}
|
||||||
|
return getNormalizedAuditPayload(logData, entityType as string, entityId as string);
|
||||||
return {
|
|
||||||
id: logData.id,
|
|
||||||
action: logData.action,
|
|
||||||
description: baseDescription,
|
|
||||||
entityType: entityType,
|
|
||||||
entityId: entityId,
|
|
||||||
userName: logData.user?.fullName || 'System',
|
|
||||||
userEmail: logData.user?.email || null,
|
|
||||||
remarks: logData.remarks || logData.newData?.remarks,
|
|
||||||
newData: details, // Normalize module-specific 'details' to 'newData' for UI
|
|
||||||
timestamp: logData.createdAt
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -217,30 +249,50 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
// Dynamic Table Switching
|
// Dynamic Table Switching
|
||||||
if (type === 'resignation') {
|
if (type === 'resignation') {
|
||||||
totalLogs = await db.ResignationAudit.count({ where: { resignationId: entityId as string } });
|
const resignation = await db.Resignation.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedResignationId = resignation?.id || (entityId as string);
|
||||||
|
totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedResignationId } });
|
||||||
latestLog = await db.ResignationAudit.findOne({
|
latestLog = await db.ResignationAudit.findOne({
|
||||||
where: { resignationId: entityId as string },
|
where: { resignationId: resolvedResignationId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else if (type === 'termination') {
|
} else if (type === 'termination') {
|
||||||
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: entityId as string } });
|
const termination = await db.TerminationRequest.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedTerminationId = termination?.id || (entityId as string);
|
||||||
|
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedTerminationId } });
|
||||||
latestLog = await db.TerminationAudit.findOne({
|
latestLog = await db.TerminationAudit.findOne({
|
||||||
where: { terminationRequestId: entityId as string },
|
where: { terminationRequestId: resolvedTerminationId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else if (type === 'fnf') {
|
} else if (type === 'fnf') {
|
||||||
totalLogs = await db.FnFAudit.count({ where: { fnfId: entityId as string } });
|
const fnf = await db.FnF.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedFnfId = fnf?.id || (entityId as string);
|
||||||
|
totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedFnfId } });
|
||||||
latestLog = await db.FnFAudit.findOne({
|
latestLog = await db.FnFAudit.findOne({
|
||||||
where: { fnfId: entityId as string },
|
where: { fnfId: resolvedFnfId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else if (type === 'constitutional' || type === 'constitutional_change') {
|
} else if (type === 'constitutional' || type === 'constitutional_change') {
|
||||||
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: entityId as string } });
|
const constitutional = await db.ConstitutionalChange.findOne({
|
||||||
|
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
|
||||||
|
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedConstitutionalId } });
|
||||||
latestLog = await db.ConstitutionalAudit.findOne({
|
latestLog = await db.ConstitutionalAudit.findOne({
|
||||||
where: { constitutionalChangeId: entityId as string },
|
where: { constitutionalChangeId: resolvedConstitutionalId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
|
|||||||
@ -214,7 +214,7 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
|
|||||||
if (checklist) {
|
if (checklist) {
|
||||||
if (checklist.applicationId) {
|
if (checklist.applicationId) {
|
||||||
await db.Application.update({
|
await db.Application.update({
|
||||||
overallStatus: 'Approved',
|
overallStatus: 'Inauguration',
|
||||||
progressPercentage: 100
|
progressPercentage: 100
|
||||||
}, { where: { id: checklist.applicationId } });
|
}, { where: { id: checklist.applicationId } });
|
||||||
|
|
||||||
|
|||||||
@ -153,3 +153,38 @@ export const uploadReport = async (req: AuthRequest, res: Response) => {
|
|||||||
res.status(500).json({ success: false, message: 'Error uploading report' });
|
res.status(500).json({ success: false, message: 'Error uploading report' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const flagNonResponsive = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { applicationId, remarks } = req.body;
|
||||||
|
const targetId = applicationId as string;
|
||||||
|
|
||||||
|
// Resolve application first to get UUID
|
||||||
|
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(targetId);
|
||||||
|
const application = await Application.findOne({
|
||||||
|
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Update Application status at model level
|
||||||
|
await application.update({ statutoryStatus: 'Flagged' });
|
||||||
|
|
||||||
|
// 2. Add high-level Audit Log entry (Using literal 'UPDATED' to absolutely avoid ENUM errors)
|
||||||
|
console.log(`[FDDController] Flagging application ${application.id} with action: UPDATED`);
|
||||||
|
await AuditLog.create({
|
||||||
|
userId: req.user?.id,
|
||||||
|
action: 'UPDATED',
|
||||||
|
entityType: 'application',
|
||||||
|
entityId: application.id,
|
||||||
|
newData: { statutoryStatus: 'Flagged', remarks: remarks || 'Applicant is non-responsive to FDD queries.' }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Application flagged successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Flag non-responsive error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error highlighting non-responsiveness' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -8,5 +8,6 @@ router.use(authenticate as any);
|
|||||||
router.get('/:applicationId', fddController.getAssignment);
|
router.get('/:applicationId', fddController.getAssignment);
|
||||||
router.post('/assign', fddController.assignAgency);
|
router.post('/assign', fddController.assignAgency);
|
||||||
router.post('/report', fddController.uploadReport);
|
router.post('/report', fddController.uploadReport);
|
||||||
|
router.post('/flag', fddController.flagNonResponsive);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { AuthRequest } from '../../types/express.types.js';
|
|||||||
import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js';
|
import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js';
|
||||||
import { syncLocationManagers } from '../master/syncHierarchy.service.js';
|
import { syncLocationManagers } from '../master/syncHierarchy.service.js';
|
||||||
import { WorkflowService } from '../../services/WorkflowService.js';
|
import { WorkflowService } from '../../services/WorkflowService.js';
|
||||||
|
import { WorkflowIntegrityService } from '../../services/WorkflowIntegrityService.js';
|
||||||
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||||
|
|
||||||
const { DocumentStageConfig } = db;
|
const { DocumentStageConfig } = db;
|
||||||
@ -154,7 +155,8 @@ export const getApplications = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
// Security Check: If prospective dealer, only show their own application
|
// Security Check: If prospective dealer, only show their own application
|
||||||
if (req.user?.roleCode === 'Prospective Dealer') {
|
if (req.user?.roleCode === 'Prospective Dealer') {
|
||||||
whereClause.email = req.user.email;
|
// Filter by phone instead of email to show all applications from same user
|
||||||
|
whereClause.phone = (req.user as any).phone || req.user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security Check: If FDD user, only show applications where they are a participant
|
// Security Check: If FDD user, only show applications where they are a participant
|
||||||
@ -197,6 +199,9 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
|
|||||||
where.applicationId = targetId;
|
where.applicationId = targetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PROACTIVE INTEGRITY CHECK: Ensure application isn't stalled before returning
|
||||||
|
await WorkflowIntegrityService.synchronizeApplicationState(targetId);
|
||||||
|
|
||||||
const application = await Application.findOne({
|
const application = await Application.findOne({
|
||||||
where,
|
where,
|
||||||
include: [
|
include: [
|
||||||
@ -297,7 +302,19 @@ export const getApplicationById = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
// Security Check: Ensure prospective dealer controls data ownership and document privacy
|
// Security Check: Ensure prospective dealer controls data ownership and document privacy
|
||||||
if (req.user?.roleCode === 'Prospective Dealer') {
|
if (req.user?.roleCode === 'Prospective Dealer') {
|
||||||
if (application.email !== req.user.email) {
|
const userEmail = req.user.email;
|
||||||
|
const userPhone = (req.user as any).phone;
|
||||||
|
|
||||||
|
// Helper to normalize phone for comparison (last 10 digits)
|
||||||
|
const normalize = (p: string) => p ? String(p).replace(/[^0-9]/g, '').slice(-10) : '';
|
||||||
|
const normalizedAppPhone = normalize(application.phone);
|
||||||
|
const normalizedUserPhone = normalize(userPhone);
|
||||||
|
|
||||||
|
const hasAccess =
|
||||||
|
(application.email && userEmail && application.email.toLowerCase() === userEmail.toLowerCase()) ||
|
||||||
|
(normalizedAppPhone && normalizedUserPhone && normalizedAppPhone === normalizedUserPhone);
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' });
|
return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -851,6 +868,34 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
|
|||||||
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
|
where: isUUID ? { [Op.or]: [{ id: targetId }, { applicationId: targetId }] } : { applicationId: targetId }
|
||||||
});
|
});
|
||||||
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
|
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
|
|
||||||
|
// Strict Workflow Validation: Dealer Code Generation requires LOI Issued status
|
||||||
|
if (application.overallStatus !== APPLICATION_STATUS.LOI_ISSUED && application.overallStatus !== APPLICATION_STATUS.DEALER_CODE_GENERATION) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Cannot generate dealer codes. The application must be in 'LOI Issued' status (Current: ${application.overallStatus}).`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: Check for mandatory fields before triggering Dealer Code Generation
|
||||||
|
const mandatoryFields = [
|
||||||
|
{ key: 'panNumber', label: 'PAN Number' },
|
||||||
|
{ key: 'gstNumber', label: 'GST Number' },
|
||||||
|
{ key: 'bankName', label: 'Bank Name' },
|
||||||
|
{ key: 'accountNumber', label: 'Account Number' },
|
||||||
|
{ key: 'ifscCode', label: 'IFSC Code' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const missingFields = mandatoryFields
|
||||||
|
.filter(f => !application[f.key as keyof typeof application])
|
||||||
|
.map(f => f.label);
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Cannot generate dealer codes. Missing mandatory fields: ${missingFields.join(', ')}. Please update application details first.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger Mock SAP Integration
|
// Trigger Mock SAP Integration
|
||||||
const { data: sapData } = await ExternalMocksService.mockGenerateSapCodes(application.applicationId);
|
const { data: sapData } = await ExternalMocksService.mockGenerateSapCodes(application.applicationId);
|
||||||
|
|||||||
@ -17,13 +17,15 @@ export class ProspectiveLoginController {
|
|||||||
|
|
||||||
console.log(`[ProspectiveLogin] Received OTP request for phone: '${phone}'`);
|
console.log(`[ProspectiveLogin] Received OTP request for phone: '${phone}'`);
|
||||||
|
|
||||||
// Check if application exists and is shortlisted
|
// Check if ANY application exists for this phone and prioritize shortlisted ones
|
||||||
const application = await db.Application.findOne({
|
const application = await db.Application.findOne({
|
||||||
where: { phone: phone }
|
where: { phone: phone },
|
||||||
|
order: [
|
||||||
|
[db.sequelize.literal("CASE WHEN \"isShortlisted\" = true OR \"ddLeadShortlisted\" = true THEN 0 ELSE 1 END"), 'ASC'],
|
||||||
|
['createdAt', 'DESC']
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[ProspectiveLogin] DB Search Result:`, application ? `Found AppId: ${application.id}, Shortlisted: ${application.isShortlisted}, DDLeadShortlisted: ${application.ddLeadShortlisted}` : 'Not Found');
|
|
||||||
|
|
||||||
if (!application) {
|
if (!application) {
|
||||||
console.log(`[ProspectiveLogin] Application not found for ${phone}, returning 404`);
|
console.log(`[ProspectiveLogin] Application not found for ${phone}, returning 404`);
|
||||||
return res.status(404).json({ message: 'No application found with this phone number' });
|
return res.status(404).json({ message: 'No application found with this phone number' });
|
||||||
@ -60,9 +62,13 @@ export class ProspectiveLoginController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (otp === '123456') {
|
if (otp === '123456') {
|
||||||
// Fetch application again to get details
|
// Fetch the latest shortlisted application again to get details for the session
|
||||||
const application = await db.Application.findOne({
|
const application = await db.Application.findOne({
|
||||||
where: { phone: phone }
|
where: { phone: phone },
|
||||||
|
order: [
|
||||||
|
[db.sequelize.literal("CASE WHEN \"isShortlisted\" = true OR \"ddLeadShortlisted\" = true THEN 0 ELSE 1 END"), 'ASC'],
|
||||||
|
['createdAt', 'DESC']
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!application) {
|
if (!application) {
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import { Response, NextFunction } from 'express';
|
import { Response, NextFunction } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
import logger from '../../common/utils/logger.js';
|
import logger from '../../common/utils/logger.js';
|
||||||
import { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
|
import {
|
||||||
|
RESIGNATION_STAGES,
|
||||||
|
AUDIT_ACTIONS,
|
||||||
|
ROLES,
|
||||||
|
REQUEST_TYPES,
|
||||||
|
RESIGNATION_DOCUMENT_TYPES,
|
||||||
|
RESIGNATION_DOCUMENT_STAGES
|
||||||
|
} from '../../common/config/constants.js';
|
||||||
import { Op, Transaction } from 'sequelize';
|
import { Op, Transaction } from 'sequelize';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
||||||
@ -9,6 +16,7 @@ import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
|||||||
import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
|
import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
|
||||||
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||||
|
import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||||
|
|
||||||
// Removed generateResignationId and moved to NomenclatureService
|
// Removed generateResignationId and moved to NomenclatureService
|
||||||
|
|
||||||
@ -51,7 +59,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
|
|||||||
reason,
|
reason,
|
||||||
additionalInfo,
|
additionalInfo,
|
||||||
currentStage: RESIGNATION_STAGES.ASM,
|
currentStage: RESIGNATION_STAGES.ASM,
|
||||||
status: 'ASM Review',
|
status: getResignationStatusForStage(RESIGNATION_STAGES.ASM),
|
||||||
progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM),
|
progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM),
|
||||||
submittedOn: new Date(),
|
submittedOn: new Date(),
|
||||||
documents: [],
|
documents: [],
|
||||||
@ -184,6 +192,80 @@ export const getResignationById = async (req: AuthRequest, res: Response, next:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploadResignationDocument = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
|
if (!req.file) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({ success: false, message: 'File is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const { documentType = RESIGNATION_DOCUMENT_TYPES[0], stage = null } = req.body;
|
||||||
|
if (!RESIGNATION_DOCUMENT_TYPES.includes(documentType)) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Invalid document type. Allowed values: ${RESIGNATION_DOCUMENT_TYPES.join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (stage && !RESIGNATION_DOCUMENT_STAGES.includes(stage)) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const idStr = String(id);
|
||||||
|
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
||||||
|
const resignation = await db.Resignation.findOne({
|
||||||
|
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resignation) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(404).json({ success: false, message: 'Resignation not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = `/uploads/documents/${req.file.filename}`;
|
||||||
|
const document = await db.ResignationDocument.create({
|
||||||
|
resignationId: resignation.id,
|
||||||
|
documentType,
|
||||||
|
fileName: req.file.originalname,
|
||||||
|
filePath,
|
||||||
|
fileSize: req.file.size,
|
||||||
|
mimeType: req.file.mimetype,
|
||||||
|
stage,
|
||||||
|
uploadedBy: req.user.id
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await db.ResignationAudit.create({
|
||||||
|
userId: req.user.id,
|
||||||
|
resignationId: resignation.id,
|
||||||
|
action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
|
||||||
|
remarks: `${documentType} uploaded`,
|
||||||
|
details: { fileName: req.file.originalname, stage, documentType }
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
const timeline = [...(resignation.timeline || []), {
|
||||||
|
stage: resignation.currentStage,
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
action: `Document uploaded: ${documentType}`,
|
||||||
|
remarks: req.file.originalname
|
||||||
|
}];
|
||||||
|
await resignation.update({ timeline }, { transaction });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.status(201).json({ success: true, message: 'Document uploaded successfully', document });
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
logger.error('Error uploading resignation document:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Approve resignation (move to next stage)
|
// Approve resignation (move to next stage)
|
||||||
export const approveResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const approveResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
const targetOverride = (req as any).targetStage;
|
const targetOverride = (req as any).targetStage;
|
||||||
@ -237,7 +319,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
// Transition via Workflow Service
|
// Transition via Workflow Service
|
||||||
await ResignationWorkflowService.transitionResignation(resignation, nextStage, req.user.id, {
|
await ResignationWorkflowService.transitionResignation(resignation, nextStage, req.user.id, {
|
||||||
remarks,
|
remarks,
|
||||||
status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`
|
status: getResignationStatusForStage(nextStage)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Special logic for F&F and Completion
|
// Special logic for F&F and Completion
|
||||||
@ -426,7 +508,7 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
await ResignationWorkflowService.transitionResignation(resignation, prevStage, req.user.id, {
|
await ResignationWorkflowService.transitionResignation(resignation, prevStage, req.user.id, {
|
||||||
remarks,
|
remarks,
|
||||||
action: 'Sent Back',
|
action: 'Sent Back',
|
||||||
status: `${prevStage} Review (Sent Back)`
|
status: `${getResignationStatusForStage(prevStage)} (Sent Back)`
|
||||||
});
|
});
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
@ -438,7 +520,107 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update departmental clearance
|
// Update departmental clearance (existing code)...
|
||||||
|
|
||||||
|
// Manually assign participant
|
||||||
|
export const assignResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
|
const { id } = req.params;
|
||||||
|
const { assignTo, remarks } = req.body; // assignTo is a role code or specific userId
|
||||||
|
|
||||||
|
const resignation = await db.Resignation.findOne({
|
||||||
|
where: { [Op.or]: [{ id }, { resignationId: id }] },
|
||||||
|
include: [{ model: db.User, as: 'dealer' }]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resignation) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Resignation not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetUserId = null;
|
||||||
|
|
||||||
|
// If assignTo is a UUID, it's a direct user assignment
|
||||||
|
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(assignTo);
|
||||||
|
|
||||||
|
if (isUUID) {
|
||||||
|
targetUserId = assignTo;
|
||||||
|
} else {
|
||||||
|
// Role-based resolution
|
||||||
|
const user = await db.User.findByPk(resignation.dealerId);
|
||||||
|
if (user && user.dealerId) {
|
||||||
|
const dealer = await db.Dealer.findByPk(user.dealerId, {
|
||||||
|
include: [{
|
||||||
|
model: db.Application,
|
||||||
|
as: 'application',
|
||||||
|
include: [{
|
||||||
|
model: db.District,
|
||||||
|
as: 'district',
|
||||||
|
include: [{ model: db.Region, as: 'region' }, { model: db.Zone, as: 'zone' }]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dealer?.application?.district) {
|
||||||
|
const d = dealer.application.district;
|
||||||
|
if (assignTo === 'asm') targetUserId = d.asmId;
|
||||||
|
else if (assignTo === 'rbm') targetUserId = d.region?.rbmId;
|
||||||
|
else if (assignTo === 'zbh') targetUserId = d.zone?.zbhId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for national roles
|
||||||
|
if (!targetUserId) {
|
||||||
|
const roleIdMap: Record<string, string> = {
|
||||||
|
'nbh': ROLES.NBH,
|
||||||
|
'legal': ROLES.LEGAL_ADMIN,
|
||||||
|
'dd_admin': ROLES.DD_ADMIN
|
||||||
|
};
|
||||||
|
const targetRole = roleIdMap[assignTo];
|
||||||
|
if (targetRole) {
|
||||||
|
const roleUser = await db.User.findOne({ where: { roleCode: targetRole, status: 'active' } });
|
||||||
|
if (roleUser) targetUserId = roleUser.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetUserId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Could not resolve a unique user for assignment: ${assignTo}. Please ensure the underlying master data (District/Region/Zone) is correctly mapped.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.RequestParticipant.findOrCreate({
|
||||||
|
where: {
|
||||||
|
requestId: resignation.id,
|
||||||
|
requestType: REQUEST_TYPES.RESIGNATION,
|
||||||
|
userId: targetUserId
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
participantType: 'contributor',
|
||||||
|
joinedMethod: 'manual',
|
||||||
|
metadata: {
|
||||||
|
assignedBy: req.user.id,
|
||||||
|
remarks: remarks || 'Manual assignment'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.ResignationAudit.create({
|
||||||
|
userId: req.user.id,
|
||||||
|
resignationId: resignation.id,
|
||||||
|
action: AUDIT_ACTIONS.UPDATED,
|
||||||
|
remarks: `Manually assigned user to the request. ${remarks || ''}`,
|
||||||
|
details: { assignedUserId: targetUserId, roleToAssign: assignTo }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Participant assigned successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error assigning resignation:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
export const updateClearance = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const updateClearance = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
const transaction: Transaction = await db.sequelize.transaction();
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
@ -495,16 +677,8 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
// Sync with F&F Clearance if settlement exists
|
// Sync with F&F Clearance if settlement exists
|
||||||
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } });
|
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } });
|
||||||
if (fnf) {
|
if (fnf) {
|
||||||
// Mapping UI status to F&F status
|
|
||||||
// Mapping status to F&F status based on amount logic
|
|
||||||
let fnfStatus = 'Pending';
|
|
||||||
const numAmount = parseFloat(amount) || 0;
|
const numAmount = parseFloat(amount) || 0;
|
||||||
|
const fnfStatus = normalizeClearanceStatus(status, numAmount);
|
||||||
if (numAmount === 0) {
|
|
||||||
fnfStatus = 'NOC Submitted';
|
|
||||||
} else {
|
|
||||||
fnfStatus = 'Dues Pending';
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingClearance = await db.FffClearance.findOne({
|
const existingClearance = await db.FffClearance.findOne({
|
||||||
where: { fnfId: fnf.id, department },
|
where: { fnfId: fnf.id, department },
|
||||||
@ -619,6 +793,9 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
(req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED;
|
(req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED;
|
||||||
return approveResignation(req, res, next);
|
return approveResignation(req, res, next);
|
||||||
|
|
||||||
|
case 'assign':
|
||||||
|
return assignResignation(req, res, next);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@ -21,5 +21,6 @@ router.put('/:id/sendback', authenticate as any, resignationController.sendBackR
|
|||||||
router.post('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
|
router.post('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
|
||||||
|
|
||||||
router.put('/:id/clearance', authenticate as any, uploadSingle, resignationController.updateClearance);
|
router.put('/:id/clearance', authenticate as any, uploadSingle, resignationController.updateClearance);
|
||||||
|
router.post('/:id/documents', authenticate as any, uploadSingle, resignationController.uploadResignationDocument);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { AuthRequest } from '../../types/express.types.js';
|
|||||||
import { FNF_STATUS, AUDIT_ACTIONS, FNF_DEPARTMENTS, RESIGNATION_STAGES, TERMINATION_STAGES } from '../../common/config/constants.js';
|
import { FNF_STATUS, AUDIT_ACTIONS, FNF_DEPARTMENTS, RESIGNATION_STAGES, TERMINATION_STAGES } from '../../common/config/constants.js';
|
||||||
import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
|
import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
|
||||||
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
|
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
|
||||||
|
import { normalizeFnFStatus, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||||
|
|
||||||
export const getDepartments = async (req: Request, res: Response) => {
|
export const getDepartments = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@ -75,8 +76,9 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
|
|||||||
const fnf = await FnF.findByPk(id);
|
const fnf = await FnF.findByPk(id);
|
||||||
if (!fnf) return res.status(404).json({ success: false, message: 'F&F settlement not found' });
|
if (!fnf) return res.status(404).json({ success: false, message: 'F&F settlement not found' });
|
||||||
|
|
||||||
|
const normalizedStatus = normalizeFnFStatus(status || fnf.status);
|
||||||
await fnf.update({
|
await fnf.update({
|
||||||
status: status || fnf.status,
|
status: normalizedStatus,
|
||||||
netAmount: finalSettlementAmount || fnf.netAmount,
|
netAmount: finalSettlementAmount || fnf.netAmount,
|
||||||
settlementAmount: finalSettlementAmount || fnf.settlementAmount,
|
settlementAmount: finalSettlementAmount || fnf.settlementAmount,
|
||||||
settlementDate: settlementDate || fnf.settlementDate,
|
settlementDate: settlementDate || fnf.settlementDate,
|
||||||
@ -91,21 +93,29 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
|
|||||||
action: AUDIT_ACTIONS.FNF_UPDATED,
|
action: AUDIT_ACTIONS.FNF_UPDATED,
|
||||||
entityType: 'fnf',
|
entityType: 'fnf',
|
||||||
entityId: id,
|
entityId: id,
|
||||||
newData: { status, netAmount: finalSettlementAmount, remarks }
|
newData: { status: normalizedStatus, netAmount: finalSettlementAmount, remarks }
|
||||||
});
|
});
|
||||||
|
|
||||||
// If status is being set to Completed, update the parent request status as well
|
// If status is being set to Completed, transition parent request via workflow services
|
||||||
if (status === 'Completed' || status === FNF_STATUS.COMPLETED) {
|
if (normalizedStatus === FNF_STATUS.COMPLETED) {
|
||||||
if (fnf.resignationId) {
|
if (fnf.resignationId) {
|
||||||
await Resignation.update(
|
const resignation = await Resignation.findByPk(fnf.resignationId);
|
||||||
{ status: 'Completed', stage: 'Completed' },
|
if (resignation) {
|
||||||
{ where: { id: fnf.resignationId } }
|
await ResignationWorkflowService.transitionResignation(resignation, RESIGNATION_STAGES.COMPLETED, req.user?.id || null, {
|
||||||
);
|
action: 'F&F Settlement Completed',
|
||||||
|
remarks: remarks || 'F&F marked completed from settlement module.',
|
||||||
|
status: 'Completed'
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (fnf.terminationRequestId) {
|
} else if (fnf.terminationRequestId) {
|
||||||
await TerminationRequest.update(
|
const termination = await TerminationRequest.findByPk(fnf.terminationRequestId);
|
||||||
{ status: 'Completed' },
|
if (termination) {
|
||||||
{ where: { id: fnf.terminationRequestId } }
|
await TerminationWorkflowService.transitionTermination(termination, TERMINATION_STAGES.TERMINATED, req.user?.id || null, {
|
||||||
);
|
action: 'F&F Settlement Completed',
|
||||||
|
remarks: remarks || 'F&F marked completed from settlement module.',
|
||||||
|
status: 'Terminated'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,7 +315,7 @@ const calculateFnFLogic = async (id: string, userId: string | null = null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine Overall F&F Status
|
// Determine Overall F&F Status
|
||||||
let newStatus = fnf.status;
|
let newStatus = normalizeFnFStatus(fnf.status);
|
||||||
if (fnf.status === FNF_STATUS.INITIATED && progressPercentage > 0) {
|
if (fnf.status === FNF_STATUS.INITIATED && progressPercentage > 0) {
|
||||||
newStatus = FNF_STATUS.DD_CLEARANCE;
|
newStatus = FNF_STATUS.DD_CLEARANCE;
|
||||||
}
|
}
|
||||||
@ -348,17 +358,21 @@ const calculateFnFLogic = async (id: string, userId: string | null = null) => {
|
|||||||
export const updateClearance = async (req: AuthRequest, res: Response) => {
|
export const updateClearance = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id, clearanceId } = req.params;
|
const { id, clearanceId } = req.params;
|
||||||
const { status, remarks, documentId, supportingDocument } = req.body;
|
const body = (req.body || {}) as Record<string, any>;
|
||||||
|
const { status, remarks, documentId, supportingDocument } = body;
|
||||||
const clearance = await FffClearance.findOne({ where: { id: clearanceId, fnfId: id } });
|
const clearance = await FffClearance.findOne({ where: { id: clearanceId, fnfId: id } });
|
||||||
if (!clearance) return res.status(404).json({ success: false, message: 'Clearance record not found' });
|
if (!clearance) return res.status(404).json({ success: false, message: 'Clearance record not found' });
|
||||||
|
|
||||||
|
const uploadedSupportingDocument = req.file ? `/uploads/documents/${req.file.filename}` : undefined;
|
||||||
|
|
||||||
|
const normalizedStatus = normalizeClearanceStatus(status || clearance.status, Number(clearance.amount || 0));
|
||||||
await clearance.update({
|
await clearance.update({
|
||||||
status: status || clearance.status,
|
status: normalizedStatus,
|
||||||
remarks: remarks || clearance.remarks,
|
remarks: remarks || clearance.remarks,
|
||||||
documentId: documentId || clearance.documentId,
|
documentId: documentId || clearance.documentId,
|
||||||
supportingDocument: supportingDocument || clearance.supportingDocument,
|
supportingDocument: uploadedSupportingDocument || supportingDocument || clearance.supportingDocument,
|
||||||
clearedBy: req.user?.id,
|
clearedBy: req.user?.id,
|
||||||
clearedAt: status === 'Cleared' ? new Date() : clearance.clearedAt
|
clearedAt: normalizedStatus !== 'Pending' ? new Date() : clearance.clearedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
// Automatically update FnF progress
|
// Automatically update FnF progress
|
||||||
@ -373,7 +387,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
|
|||||||
fnfId: id,
|
fnfId: id,
|
||||||
action: 'CLEARANCE_UPDATED',
|
action: 'CLEARANCE_UPDATED',
|
||||||
remarks: remarks || 'No remarks',
|
remarks: remarks || 'No remarks',
|
||||||
details: { department: clearance.department, status }
|
details: { department: clearance.department, status: normalizedStatus }
|
||||||
});
|
});
|
||||||
} catch (auditError) {
|
} catch (auditError) {
|
||||||
console.error('[SettlementController] Local FnFAudit creation failed:', auditError);
|
console.error('[SettlementController] Local FnFAudit creation failed:', auditError);
|
||||||
@ -393,7 +407,7 @@ export const updateClearance = async (req: AuthRequest, res: Response) => {
|
|||||||
[parentKey]: parentId,
|
[parentKey]: parentId,
|
||||||
action: 'STAKEHOLDER_CLEARANCE_UPDATED',
|
action: 'STAKEHOLDER_CLEARANCE_UPDATED',
|
||||||
remarks: `Automated sync from F&F: ${remarks || 'No remarks'}`,
|
remarks: `Automated sync from F&F: ${remarks || 'No remarks'}`,
|
||||||
details: { department: clearance.department, status }
|
details: { department: clearance.department, status: normalizedStatus }
|
||||||
});
|
});
|
||||||
} catch (parentAuditError) {
|
} catch (parentAuditError) {
|
||||||
console.error('[SettlementController] Parent Audit creation failed:', parentAuditError);
|
console.error('[SettlementController] Parent Audit creation failed:', parentAuditError);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import * as settlementController from './settlement.controller.js';
|
|||||||
import { authenticate } from '../../common/middleware/auth.js';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
import { checkRole } from '../../common/middleware/roleCheck.js';
|
import { checkRole } from '../../common/middleware/roleCheck.js';
|
||||||
import { ROLES } from '../../common/config/constants.js';
|
import { ROLES } from '../../common/config/constants.js';
|
||||||
|
import { uploadSingle } from '../../common/middleware/upload.js';
|
||||||
|
|
||||||
// All routes require authentication
|
// All routes require authentication
|
||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
@ -19,7 +20,7 @@ router.put('/payments/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any
|
|||||||
router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.updateFnF);
|
router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.updateFnF);
|
||||||
router.post('/fnf/:id/calculate', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.calculateFnF);
|
router.post('/fnf/:id/calculate', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.calculateFnF);
|
||||||
|
|
||||||
router.put('/fnf/:id/clearances/:clearanceId', checkRole([ROLES.FINANCE, ROLES.SPARES_MANAGER, ROLES.SERVICE_MANAGER, ROLES.ACCOUNTS_MANAGER, ROLES.SUPER_ADMIN]) as any, settlementController.updateClearance);
|
router.put('/fnf/:id/clearances/:clearanceId', uploadSingle, checkRole([ROLES.FINANCE, ROLES.SPARES_MANAGER, ROLES.SERVICE_MANAGER, ROLES.ACCOUNTS_MANAGER, ROLES.SUPER_ADMIN]) as any, settlementController.updateClearance);
|
||||||
|
|
||||||
// Line item management
|
// Line item management
|
||||||
router.post('/fnf/:id/line-items', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.addLineItem);
|
router.post('/fnf/:id/line-items', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.addLineItem);
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { Response, NextFunction } from 'express';
|
import { Response, NextFunction } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
import logger from '../../common/utils/logger.js';
|
import logger from '../../common/utils/logger.js';
|
||||||
import { TERMINATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
|
import {
|
||||||
|
TERMINATION_STAGES,
|
||||||
|
AUDIT_ACTIONS,
|
||||||
|
ROLES,
|
||||||
|
TERMINATION_DOCUMENT_TYPES,
|
||||||
|
TERMINATION_DOCUMENT_STAGES
|
||||||
|
} from '../../common/config/constants.js';
|
||||||
import { Op, Transaction } from 'sequelize';
|
import { Op, Transaction } from 'sequelize';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
||||||
@ -9,6 +15,7 @@ import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
|||||||
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
|
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
|
||||||
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||||
|
import { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||||
|
|
||||||
// Create termination request
|
// Create termination request
|
||||||
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
@ -27,7 +34,7 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
|
|||||||
comments,
|
comments,
|
||||||
initiatedBy: req.user.id,
|
initiatedBy: req.user.id,
|
||||||
currentStage: TERMINATION_STAGES.SUBMITTED,
|
currentStage: TERMINATION_STAGES.SUBMITTED,
|
||||||
status: 'Submitted',
|
status: getTerminationStatusForStage(TERMINATION_STAGES.SUBMITTED),
|
||||||
progressPercentage: TerminationWorkflowService.calculateProgress(TERMINATION_STAGES.SUBMITTED),
|
progressPercentage: TerminationWorkflowService.calculateProgress(TERMINATION_STAGES.SUBMITTED),
|
||||||
timeline: [{
|
timeline: [{
|
||||||
stage: 'Submitted',
|
stage: 'Submitted',
|
||||||
@ -151,6 +158,80 @@ export const getTerminationById = async (req: AuthRequest, res: Response, next:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploadTerminationDocument = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
|
try {
|
||||||
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
|
if (!req.file) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({ success: false, message: 'File is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = req.params;
|
||||||
|
const { documentType = TERMINATION_DOCUMENT_TYPES[0], stage = null } = req.body;
|
||||||
|
if (!TERMINATION_DOCUMENT_TYPES.includes(documentType)) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Invalid document type. Allowed values: ${TERMINATION_DOCUMENT_TYPES.join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (stage && !TERMINATION_DOCUMENT_STAGES.includes(stage)) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const idStr = String(id);
|
||||||
|
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
||||||
|
const termination = await db.TerminationRequest.findOne({
|
||||||
|
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!termination) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(404).json({ success: false, message: 'Termination request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = `/uploads/documents/${req.file.filename}`;
|
||||||
|
const document = await db.TerminationDocument.create({
|
||||||
|
terminationRequestId: termination.id,
|
||||||
|
documentType,
|
||||||
|
fileName: req.file.originalname,
|
||||||
|
filePath,
|
||||||
|
fileSize: req.file.size,
|
||||||
|
mimeType: req.file.mimetype,
|
||||||
|
stage,
|
||||||
|
uploadedBy: req.user.id
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await db.TerminationAudit.create({
|
||||||
|
userId: req.user.id,
|
||||||
|
terminationRequestId: termination.id,
|
||||||
|
action: AUDIT_ACTIONS.DOCUMENT_UPLOADED,
|
||||||
|
remarks: `${documentType} uploaded`,
|
||||||
|
details: { fileName: req.file.originalname, stage, documentType }
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
const timeline = [...(termination.timeline || []), {
|
||||||
|
stage: termination.currentStage,
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
action: `Document uploaded: ${documentType}`,
|
||||||
|
remarks: req.file.originalname
|
||||||
|
}];
|
||||||
|
await termination.update({ timeline }, { transaction });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.status(201).json({ success: true, message: 'Document uploaded successfully', document });
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
logger.error('Error uploading termination document:', error);
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Update termination status (Approve/Reject)
|
// Update termination status (Approve/Reject)
|
||||||
export const updateTerminationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const updateTerminationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
const transaction: Transaction = await db.sequelize.transaction();
|
const transaction: Transaction = await db.sequelize.transaction();
|
||||||
@ -197,7 +278,7 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
|
|
||||||
await TerminationWorkflowService.transitionTermination(termination, nextStage, req.user.id, {
|
await TerminationWorkflowService.transitionTermination(termination, nextStage, req.user.id, {
|
||||||
remarks,
|
remarks,
|
||||||
status: nextStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : `${nextStage}`
|
status: getTerminationStatusForStage(nextStage)
|
||||||
});
|
});
|
||||||
|
|
||||||
// If Terminated, trigger F&F initiation via Workflow Service
|
// If Terminated, trigger F&F initiation via Workflow Service
|
||||||
@ -215,8 +296,8 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
await TerminationWorkflowService.initiateFnF(termination, req.user.id, transaction);
|
await TerminationWorkflowService.initiateFnF(termination, req.user.id, transaction);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`[TerminationController] Termination approved but LWD (${termination.proposedLwd}) not yet reached. F&F will be triggered on LWD.`);
|
logger.info(`[TerminationController] Termination approved but LWD (${termination.proposedLwd}) not yet reached. F&F will be triggered on LWD.`);
|
||||||
// Update status to reflect F&F is pending LWD arrivement
|
// Keep parent status aligned while waiting for LWD-triggered F&F
|
||||||
await termination.update({ status: 'Approved (F&F Pending LWD)' }, { transaction });
|
await termination.update({ status: 'Awaiting F&F (LWD Pending)' }, { transaction });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -286,8 +367,9 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
if (!termination) throw new Error('Termination request not found');
|
if (!termination) throw new Error('Termination request not found');
|
||||||
|
|
||||||
const clearances = { ...(termination.departmentalClearances || {}) };
|
const clearances = { ...(termination.departmentalClearances || {}) };
|
||||||
|
const normalizedStatus = normalizeClearanceStatus(status, Number(amount) || 0);
|
||||||
clearances[department] = {
|
clearances[department] = {
|
||||||
status,
|
status: normalizedStatus,
|
||||||
amount: Number(amount) || 0,
|
amount: Number(amount) || 0,
|
||||||
type: type || 'Receivable',
|
type: type || 'Receivable',
|
||||||
remarks: remarks || '',
|
remarks: remarks || '',
|
||||||
@ -301,7 +383,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
const fnf = await db.FnF.findOne({ where: { terminationRequestId: id } });
|
const fnf = await db.FnF.findOne({ where: { terminationRequestId: id } });
|
||||||
if (fnf) {
|
if (fnf) {
|
||||||
await db.FffClearance.update(
|
await db.FffClearance.update(
|
||||||
{ status, remarks, amount: Number(amount) || 0 },
|
{ status: normalizedStatus, remarks, amount: Number(amount) || 0 },
|
||||||
{ where: { fnfId: fnf.id, department }, transaction }
|
{ where: { fnfId: fnf.id, department }, transaction }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -311,7 +393,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
action: 'CLEARANCE_UPDATED',
|
action: 'CLEARANCE_UPDATED',
|
||||||
terminationRequestId: id,
|
terminationRequestId: id,
|
||||||
remarks: remarks || `Cleared ${department}`,
|
remarks: remarks || `Cleared ${department}`,
|
||||||
details: { department, status, amount }
|
details: { department, status: normalizedStatus, amount }
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|
||||||
if (fnf) {
|
if (fnf) {
|
||||||
@ -320,7 +402,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
fnfId: fnf.id,
|
fnfId: fnf.id,
|
||||||
action: 'CLEARANCE_UPDATED',
|
action: 'CLEARANCE_UPDATED',
|
||||||
remarks: remarks || `Departmental clearance recorded for ${department}`,
|
remarks: remarks || `Departmental clearance recorded for ${department}`,
|
||||||
details: { department, status, source: 'Termination Workflow' }
|
details: { department, status: normalizedStatus, source: 'Termination Workflow' }
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import express from 'express';
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import {
|
import {
|
||||||
createTermination, getTerminations, getTerminationById, updateTerminationStatus,
|
createTermination, getTerminations, getTerminationById, updateTerminationStatus,
|
||||||
submitScnResponse, recordPersonalHearing, updateClearance
|
submitScnResponse, recordPersonalHearing, updateClearance, uploadTerminationDocument
|
||||||
} from './termination.controller.js';
|
} from './termination.controller.js';
|
||||||
import { authenticate } from '../../common/middleware/auth.js';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
|
import { uploadSingle } from '../../common/middleware/upload.js';
|
||||||
|
|
||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
|
|
||||||
@ -16,5 +17,6 @@ router.post('/:id/status', updateTerminationStatus);
|
|||||||
router.post('/scn-response', submitScnResponse);
|
router.post('/scn-response', submitScnResponse);
|
||||||
router.post('/hearing-record', recordPersonalHearing);
|
router.post('/hearing-record', recordPersonalHearing);
|
||||||
router.put('/:id/clearance', updateClearance);
|
router.put('/:id/clearance', updateClearance);
|
||||||
|
router.post('/:id/documents', uploadSingle, uploadTerminationDocument);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -7,11 +7,13 @@ export class ConstitutionalWorkflowService {
|
|||||||
*/
|
*/
|
||||||
static async transitionRequest(request: any, targetStage: string, userId: string, options: any = {}) {
|
static async transitionRequest(request: any, targetStage: string, userId: string, options: any = {}) {
|
||||||
const { action, status, remarks, userFullName } = options;
|
const { action, status, remarks, userFullName } = options;
|
||||||
|
const sourceStage = request.currentStage;
|
||||||
|
|
||||||
const updatedTimeline = [
|
const updatedTimeline = [
|
||||||
...request.timeline,
|
...(request.timeline || []),
|
||||||
{
|
{
|
||||||
stage: targetStage,
|
stage: sourceStage, // Correctly Associate remark with the stage where action happened
|
||||||
|
targetStage: targetStage,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: userFullName || 'System',
|
user: userFullName || 'System',
|
||||||
action: action || `Moved to ${targetStage}`,
|
action: action || `Moved to ${targetStage}`,
|
||||||
@ -35,7 +37,7 @@ export class ConstitutionalWorkflowService {
|
|||||||
constitutionalChangeId: request.id,
|
constitutionalChangeId: request.id,
|
||||||
action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED,
|
action: action === 'Reject' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.UPDATED,
|
||||||
remarks: remarks || '',
|
remarks: remarks || '',
|
||||||
details: { status: updateData.status, stage: targetStage }
|
details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
|
||||||
});
|
});
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
|
|||||||
@ -25,13 +25,16 @@ export class RelocationWorkflowService {
|
|||||||
updateData.progressPercentage = progressPercentage;
|
updateData.progressPercentage = progressPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourceStage = request.currentStage;
|
||||||
|
|
||||||
// 1. Update Request Record
|
// 1. Update Request Record
|
||||||
await request.update(updateData);
|
await request.update(updateData);
|
||||||
|
|
||||||
// 2. Update Timeline (JSON array)
|
// 2. Update Timeline (JSON array)
|
||||||
const user = userId ? await User.findByPk(userId) : null;
|
const user = userId ? await User.findByPk(userId) : null;
|
||||||
const timelineEntry = {
|
const timelineEntry = {
|
||||||
stage: stage || request.currentStage,
|
stage: sourceStage, // Store the stage where the action happened
|
||||||
|
targetStage: stage || targetStatus,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: user ? user.fullName : 'System',
|
user: user ? user.fullName : 'System',
|
||||||
action: action || `Transitioned to ${targetStatus}`,
|
action: action || `Transitioned to ${targetStatus}`,
|
||||||
@ -47,7 +50,7 @@ export class RelocationWorkflowService {
|
|||||||
relocationRequestId: request.id,
|
relocationRequestId: request.id,
|
||||||
action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED,
|
action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED,
|
||||||
remarks: reason || '',
|
remarks: reason || '',
|
||||||
details: { status: targetStatus, stage: stage || request.currentStage }
|
details: { status: targetStatus, stage: sourceStage, targetStage: stage || targetStatus }
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`);
|
console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import db from '../database/models/index.js';
|
import db from '../database/models/index.js';
|
||||||
const { AuditLog, User, Worknote } = db;
|
const { AuditLog, User, Worknote } = db;
|
||||||
import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js';
|
import { AUDIT_ACTIONS, RESIGNATION_STAGES, ROLES } from '../common/config/constants.js';
|
||||||
|
import { getResignationStatusForStage } from '../common/utils/offboardingStatus.js';
|
||||||
import { NotificationService } from './NotificationService.js';
|
import { NotificationService } from './NotificationService.js';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import logger from '../common/utils/logger.js';
|
import logger from '../common/utils/logger.js';
|
||||||
@ -12,21 +13,20 @@ export class ResignationWorkflowService {
|
|||||||
*/
|
*/
|
||||||
static async transitionResignation(resignation: any, targetStage: string, userId: string | null = null, metadata: any = {}) {
|
static async transitionResignation(resignation: any, targetStage: string, userId: string | null = null, metadata: any = {}) {
|
||||||
const { action, remarks, status } = metadata;
|
const { action, remarks, status } = metadata;
|
||||||
|
const sourceStage = resignation.currentStage;
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
currentStage: targetStage,
|
currentStage: targetStage,
|
||||||
status: status || targetStage,
|
status: status || getResignationStatusForStage(targetStage),
|
||||||
progressPercentage: this.calculateProgress(targetStage),
|
progressPercentage: this.calculateProgress(targetStage),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Update Resignation Record
|
// 2. Update Timeline (JSON array) & Resignation Record
|
||||||
await resignation.update(updateData);
|
|
||||||
|
|
||||||
// 2. Update Timeline (JSON array)
|
|
||||||
const actor = userId ? await User.findByPk(userId) : null;
|
const actor = userId ? await User.findByPk(userId) : null;
|
||||||
const timelineEntry = {
|
const timelineEntry = {
|
||||||
stage: targetStage,
|
stage: sourceStage, // Correctly Associate remark with the stage where action happened
|
||||||
|
targetStage: targetStage, // Store target for reference
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: actor ? actor.fullName : 'System',
|
user: actor ? actor.fullName : 'System',
|
||||||
action: action || `Approved to ${targetStage}`,
|
action: action || `Approved to ${targetStage}`,
|
||||||
@ -34,7 +34,11 @@ export class ResignationWorkflowService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updatedTimeline = [...(resignation.timeline || []), timelineEntry];
|
const updatedTimeline = [...(resignation.timeline || []), timelineEntry];
|
||||||
await resignation.update({ timeline: updatedTimeline });
|
|
||||||
|
await resignation.update({
|
||||||
|
...updateData,
|
||||||
|
timeline: updatedTimeline
|
||||||
|
});
|
||||||
|
|
||||||
// 3. Create Audit Log
|
// 3. Create Audit Log
|
||||||
let auditAction: any = AUDIT_ACTIONS.APPROVED;
|
let auditAction: any = AUDIT_ACTIONS.APPROVED;
|
||||||
@ -47,7 +51,7 @@ export class ResignationWorkflowService {
|
|||||||
resignationId: resignation.id,
|
resignationId: resignation.id,
|
||||||
action: auditAction,
|
action: auditAction,
|
||||||
remarks: remarks || '',
|
remarks: remarks || '',
|
||||||
details: { status: updateData.status, stage: targetStage }
|
details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Create Worknote if it's a "Sent Back" action for communication
|
// 4. Create Worknote if it's a "Sent Back" action for communication
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import db from '../database/models/index.js';
|
|||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db;
|
const { AuditLog, User, TerminationScnResponse, TerminationHearingRecord, Dealer, FnF, FnFLineItem, FffClearance } = db;
|
||||||
import { AUDIT_ACTIONS, TERMINATION_STAGES, ROLES, FNF_DEPARTMENTS } from '../common/config/constants.js';
|
import { AUDIT_ACTIONS, TERMINATION_STAGES, ROLES, FNF_DEPARTMENTS } from '../common/config/constants.js';
|
||||||
|
import { getTerminationStatusForStage } from '../common/utils/offboardingStatus.js';
|
||||||
import { NotificationService } from './NotificationService.js';
|
import { NotificationService } from './NotificationService.js';
|
||||||
import ExternalMocksService from '../common/utils/externalMocks.service.js';
|
import ExternalMocksService from '../common/utils/externalMocks.service.js';
|
||||||
import logger from '../common/utils/logger.js';
|
import logger from '../common/utils/logger.js';
|
||||||
@ -13,20 +14,22 @@ export class TerminationWorkflowService {
|
|||||||
*/
|
*/
|
||||||
static async transitionTermination(termination: any, targetStage: string, userId: string | null = null, metadata: any = {}) {
|
static async transitionTermination(termination: any, targetStage: string, userId: string | null = null, metadata: any = {}) {
|
||||||
const { action, remarks, status } = metadata;
|
const { action, remarks, status } = metadata;
|
||||||
|
const sourceStage = termination.currentStage;
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
currentStage: targetStage,
|
currentStage: targetStage,
|
||||||
status: status || (targetStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : targetStage),
|
status: status || getTerminationStatusForStage(targetStage),
|
||||||
|
progressPercentage: this.calculateProgress(targetStage),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Update Termination Record
|
// 1. Resolve Actor
|
||||||
await termination.update(updateData);
|
|
||||||
|
|
||||||
// 2. Update Timeline (JSON array)
|
|
||||||
const actor = userId ? await User.findByPk(userId) : null;
|
const actor = userId ? await User.findByPk(userId) : null;
|
||||||
|
|
||||||
|
// 2. Prepare Timeline Entry
|
||||||
const timelineEntry = {
|
const timelineEntry = {
|
||||||
stage: targetStage,
|
stage: sourceStage, // Correctly Associate remark with the stage where action happened
|
||||||
|
targetStage: targetStage,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: actor ? actor.fullName : 'System',
|
user: actor ? actor.fullName : 'System',
|
||||||
action: action || `Approved to ${targetStage}`,
|
action: action || `Approved to ${targetStage}`,
|
||||||
@ -34,9 +37,14 @@ export class TerminationWorkflowService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updatedTimeline = [...(termination.timeline || []), timelineEntry];
|
const updatedTimeline = [...(termination.timeline || []), timelineEntry];
|
||||||
await termination.update({ timeline: updatedTimeline });
|
|
||||||
|
|
||||||
// 3. Create Audit Log
|
// 3. Perform Consolidated Update
|
||||||
|
await termination.update({
|
||||||
|
...updateData,
|
||||||
|
timeline: updatedTimeline
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Create Audit Log
|
||||||
let auditAction: any = AUDIT_ACTIONS.APPROVED;
|
let auditAction: any = AUDIT_ACTIONS.APPROVED;
|
||||||
if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED;
|
if (action === 'REJECT' || action === 'Rejected') auditAction = AUDIT_ACTIONS.REJECTED;
|
||||||
if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED;
|
if (action === 'SCN_SUBMITTED' || action === 'Hearing Recorded') auditAction = AUDIT_ACTIONS.UPDATED;
|
||||||
@ -46,7 +54,7 @@ export class TerminationWorkflowService {
|
|||||||
terminationRequestId: termination.id,
|
terminationRequestId: termination.id,
|
||||||
action: auditAction,
|
action: auditAction,
|
||||||
remarks: remarks || '',
|
remarks: remarks || '',
|
||||||
details: { status: updateData.status, stage: targetStage }
|
details: { status: updateData.status, stage: sourceStage, targetStage: targetStage }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Send Notifications
|
// 4. Send Notifications
|
||||||
|
|||||||
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 args = Object.fromEntries(
|
||||||
|
process.argv.slice(2)
|
||||||
const BASE_URL = 'http://localhost:5000/api';
|
.map(arg => arg.replace(/^--/, '').split('='))
|
||||||
|
.map(([k, v]) => [k, v ?? 'true'])
|
||||||
|
);
|
||||||
|
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
|
||||||
const PASSWORD = 'Admin@123';
|
const PASSWORD = 'Admin@123';
|
||||||
|
const STEP_DELAY_MS = Number(args.delayMs || 500);
|
||||||
|
|
||||||
const EMAILS = {
|
const EMAILS = {
|
||||||
DD_ADMIN: 'lince@gmail.com',
|
DD_ADMIN: 'lince@gmail.com',
|
||||||
@ -45,11 +49,14 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(email) {
|
async function login(email) {
|
||||||
|
if (!login.cache) login.cache = {};
|
||||||
|
if (login.cache[email]) return login.cache[email];
|
||||||
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
|
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
|
||||||
return data.token;
|
login.cache[email] = data.token;
|
||||||
|
return login.cache[email];
|
||||||
}
|
}
|
||||||
|
|
||||||
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms));
|
const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
@ -58,17 +65,21 @@ async function run() {
|
|||||||
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`);
|
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`);
|
||||||
const dealerToken = await login(EMAILS.DEALER);
|
const dealerToken = await login(EMAILS.DEALER);
|
||||||
|
|
||||||
console.log('[STEP 1] Dealer Submitting Constitutional Change...');
|
let requestId = args.requestId;
|
||||||
const createRes = await apiRequest('/self-service/constitutional', 'POST', {
|
if (!requestId) {
|
||||||
changeType: 'LLP Conversion',
|
console.log('[STEP 1] Dealer Submitting Constitutional Change...');
|
||||||
reason: 'Converting to LLP for better operational governance.',
|
const createRes = await apiRequest('/self-service/constitutional', 'POST', {
|
||||||
currentConstitution: 'Proprietorship',
|
changeType: args.changeType || 'LLP Conversion',
|
||||||
newPartnersDetails: 'John Doe, Jane Smith',
|
reason: args.reason || 'Converting to LLP for better operational governance.',
|
||||||
shareholdingPattern: '60/40'
|
currentConstitution: 'Proprietorship',
|
||||||
}, dealerToken);
|
newPartnersDetails: 'John Doe, Jane Smith',
|
||||||
|
shareholdingPattern: '60/40'
|
||||||
const requestId = createRes.requestId;
|
}, dealerToken);
|
||||||
console.log(`[STEP 1] Request Created. RequestID: ${requestId}`);
|
requestId = createRes.requestId;
|
||||||
|
console.log(`[STEP 1] Request Created. RequestID: ${requestId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[STEP 1] Resuming request: ${requestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Sequence of users taking actions to advance stages
|
// Sequence of users taking actions to advance stages
|
||||||
const approvalSequence = [
|
const approvalSequence = [
|
||||||
@ -82,8 +93,15 @@ async function run() {
|
|||||||
{ name: 'Legal Finalize', email: EMAILS.LEGAL }
|
{ name: 'Legal Finalize', email: EMAILS.LEGAL }
|
||||||
];
|
];
|
||||||
|
|
||||||
let currentStep = 2;
|
const adminToken = await login(EMAILS.DD_ADMIN);
|
||||||
for (const actor of approvalSequence) {
|
const current = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
|
||||||
|
const currentStage = current?.request?.currentStage;
|
||||||
|
const stageOrder = ['Submitted', 'ASM Review', 'ZM/RBM Review', 'ZBH Review', 'DD Lead Review', 'DD Head Review', 'NBH Approval', 'Legal Review', 'Completed'];
|
||||||
|
const startIndex = Math.max(0, stageOrder.indexOf(currentStage));
|
||||||
|
|
||||||
|
let currentStep = 2 + startIndex;
|
||||||
|
for (let i = startIndex; i < approvalSequence.length; i++) {
|
||||||
|
const actor = approvalSequence[i];
|
||||||
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`);
|
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`);
|
||||||
const token = await login(actor.email);
|
const token = await login(actor.email);
|
||||||
const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', {
|
const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', {
|
||||||
@ -96,7 +114,6 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[FINAL STEP] Verifying Completion Status...');
|
console.log('[FINAL STEP] Verifying Completion Status...');
|
||||||
const adminToken = await login(EMAILS.DD_ADMIN);
|
|
||||||
const finalDetails = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
|
const finalDetails = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
|
||||||
|
|
||||||
if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') {
|
if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') {
|
||||||
|
|||||||
156
trigger-relocation.js
Normal file
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 args = Object.fromEntries(
|
||||||
|
process.argv.slice(2)
|
||||||
const BASE_URL = 'http://localhost:5000/api';
|
.map(arg => arg.replace(/^--/, '').split('='))
|
||||||
|
.map(([k, v]) => [k, v ?? 'true'])
|
||||||
|
);
|
||||||
|
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
|
||||||
const PASSWORD = 'Admin@123';
|
const PASSWORD = 'Admin@123';
|
||||||
|
const STEP_DELAY_MS = Number(args.delayMs || 500);
|
||||||
|
const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true';
|
||||||
|
const SHOULD_SKIP_FINAL_SETTLEMENT = String(args.skipSettlement || 'false') === 'true';
|
||||||
|
|
||||||
const EMAILS = {
|
const EMAILS = {
|
||||||
DD_ADMIN: 'lince@gmail.com',
|
DD_ADMIN: 'lince@gmail.com',
|
||||||
@ -44,16 +50,19 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(email) {
|
async function login(email) {
|
||||||
|
if (!login.cache) login.cache = {};
|
||||||
|
if (login.cache[email]) return login.cache[email];
|
||||||
const isInternal = email.endsWith('@royalenfield.com') ||
|
const isInternal = email.endsWith('@royalenfield.com') ||
|
||||||
email === 'lince@gmail.com' ||
|
email === 'lince@gmail.com' ||
|
||||||
email === 'yashwin@gmail.com';
|
email === 'yashwin@gmail.com';
|
||||||
const password = isInternal ? 'Admin@123' : 'Dealer@123';
|
const password = isInternal ? 'Admin@123' : 'Dealer@123';
|
||||||
|
|
||||||
const data = await apiRequest('/auth/login', 'POST', { email, password });
|
const data = await apiRequest('/auth/login', 'POST', { email, password });
|
||||||
return data.token;
|
login.cache[email] = data.token;
|
||||||
|
return login.cache[email];
|
||||||
}
|
}
|
||||||
|
|
||||||
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms));
|
const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
|
||||||
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
|
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
@ -101,29 +110,32 @@ async function run() {
|
|||||||
console.log(`Found Target Outlet: ${targetOutlet.name} (${targetOutlet.code})`);
|
console.log(`Found Target Outlet: ${targetOutlet.name} (${targetOutlet.code})`);
|
||||||
|
|
||||||
console.log(`[STEP 1.2] Dealer Submitting Resignation for Outlet...`);
|
console.log(`[STEP 1.2] Dealer Submitting Resignation for Outlet...`);
|
||||||
let resignationId;
|
let resignationId = args.resignationId;
|
||||||
try {
|
if (!resignationId) {
|
||||||
const createRes = await apiRequest('/self-service/resignations', 'POST', {
|
try {
|
||||||
outletId: targetOutlet.id,
|
const createRes = await apiRequest('/self-service/resignations', 'POST', {
|
||||||
resignationType: 'Voluntary',
|
outletId: targetOutlet.id,
|
||||||
lastOperationalDateSales: new Date().toISOString().split('T')[0],
|
resignationType: 'Voluntary',
|
||||||
lastOperationalDateServices: new Date().toISOString().split('T')[0],
|
lastOperationalDateSales: new Date().toISOString().split('T')[0],
|
||||||
reason: 'Focusing on other business ventures',
|
lastOperationalDateServices: new Date().toISOString().split('T')[0],
|
||||||
remarks: 'Initiating voluntary resignation for E2E validation.'
|
reason: 'Focusing on other business ventures',
|
||||||
}, dealerToken);
|
remarks: 'Initiating voluntary resignation for E2E validation.'
|
||||||
resignationId = createRes.resignation.id;
|
}, dealerToken);
|
||||||
log(1, `Resignation Created. ID: ${resignationId}`);
|
resignationId = createRes.resignation.id;
|
||||||
} catch (e) {
|
log(1, `Resignation Created. ID: ${resignationId}`);
|
||||||
if (e.message.includes('already has an active resignation request')) {
|
} catch (e) {
|
||||||
console.log(`[STEP 1.2] Active resignation already exists. Fetching...`);
|
if (e.message.includes('already has an active resignation request')) {
|
||||||
// Use plural route for listing
|
console.log(`[STEP 1.2] Active resignation already exists. Fetching...`);
|
||||||
const activeResRes = await apiRequest('/self-service/resignations', 'GET', null, dealerToken);
|
const activeResRes = await apiRequest('/self-service/resignations', 'GET', null, dealerToken);
|
||||||
const activeRes = (activeResRes.resignations || activeResRes.data).find(r => r.outletId === targetOutlet.id && !['Completed', 'Rejected'].includes(r.status));
|
const activeRes = (activeResRes.resignations || activeResRes.data).find(r => r.outletId === targetOutlet.id && !['Completed', 'Rejected'].includes(r.status));
|
||||||
resignationId = activeRes.id;
|
resignationId = activeRes.id;
|
||||||
log(1, `Resuming with existig Resignation: ${resignationId}`);
|
log(1, `Resuming with existing Resignation: ${resignationId}`);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log(1, `Resuming provided resignation: ${resignationId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await delay();
|
await delay();
|
||||||
@ -165,7 +177,9 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) ---
|
// --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) ---
|
||||||
console.log('[STEP 9] Starting 16-Department F&F Clearance Flow...');
|
if (!SHOULD_SKIP_CLEARANCES) {
|
||||||
|
console.log('[STEP 9] Starting 16-Department F&F Clearance Flow...');
|
||||||
|
}
|
||||||
|
|
||||||
// Re-fetch to ensure we have the F&F ID regardless of start point
|
// Re-fetch to ensure we have the F&F ID regardless of start point
|
||||||
const finalResData = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken);
|
const finalResData = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken);
|
||||||
@ -199,32 +213,36 @@ async function run() {
|
|||||||
{ name: 'Customer Relations Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'Customer complaints resolved.' }
|
{ name: 'Customer Relations Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'Customer complaints resolved.' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const dept of departments) {
|
if (!SHOULD_SKIP_CLEARANCES) {
|
||||||
log('9.1', `Clearing Dept: ${dept.name} [${dept.status}] - ${dept.remarks}`);
|
for (const dept of departments) {
|
||||||
await apiRequest(`/self-service/resignations/${resignationId}/clearance`, 'PUT', {
|
log('9.1', `Clearing Dept: ${dept.name} [${dept.status}] - ${dept.remarks}`);
|
||||||
department: dept.name,
|
await apiRequest(`/self-service/resignations/${resignationId}/clearance`, 'PUT', {
|
||||||
status: dept.status,
|
department: dept.name,
|
||||||
remarks: dept.remarks,
|
status: dept.status,
|
||||||
amount: dept.amount,
|
remarks: dept.remarks,
|
||||||
type: dept.type
|
amount: dept.amount,
|
||||||
}, adminToken);
|
type: dept.type
|
||||||
await delay(100);
|
}, adminToken);
|
||||||
|
await delay(100);
|
||||||
|
}
|
||||||
|
log(9, 'All 16 Departments Cleared.');
|
||||||
|
await delay();
|
||||||
}
|
}
|
||||||
log(9, 'All 16 Departments Cleared.');
|
|
||||||
await delay();
|
|
||||||
|
|
||||||
// --- FINAL FINANCE SETTLEMENT ---
|
// --- FINAL FINANCE SETTLEMENT ---
|
||||||
console.log('[STEP 10] Finance Finalizing Settlement...');
|
if (!SHOULD_SKIP_FINAL_SETTLEMENT) {
|
||||||
const financeToken = await login(EMAILS.FINANCE);
|
console.log('[STEP 10] Finance Finalizing Settlement...');
|
||||||
await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', {
|
const financeToken = await login(EMAILS.FINANCE);
|
||||||
status: 'Completed',
|
await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', {
|
||||||
finalSettlementAmount: 415173, // Matches your observed amount
|
status: 'Completed',
|
||||||
paymentMode: 'NEFT / Bank Transfer',
|
finalSettlementAmount: Number(args.finalSettlementAmount || 415173),
|
||||||
transactionReference: `TXN-${Date.now()}`,
|
paymentMode: 'NEFT / Bank Transfer',
|
||||||
settlementDate: new Date().toISOString(),
|
transactionReference: `TXN-${Date.now()}`,
|
||||||
remarks: 'Settlement completed and verified via automated script.'
|
settlementDate: new Date().toISOString(),
|
||||||
}, financeToken);
|
remarks: 'Settlement completed and verified via automated script.'
|
||||||
await delay();
|
}, financeToken);
|
||||||
|
await delay();
|
||||||
|
}
|
||||||
|
|
||||||
// --- FINAL COMPLETION ---
|
// --- FINAL COMPLETION ---
|
||||||
console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...');
|
console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...');
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import fs from 'fs';
|
const args = Object.fromEntries(
|
||||||
|
process.argv.slice(2)
|
||||||
const BASE_URL = 'http://localhost:5000/api';
|
.map(arg => arg.replace(/^--/, '').split('='))
|
||||||
|
.map(([k, v]) => [k, v ?? 'true'])
|
||||||
|
);
|
||||||
|
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
|
||||||
const PASSWORD = 'Admin@123';
|
const PASSWORD = 'Admin@123';
|
||||||
|
const STEP_DELAY_MS = Number(args.delayMs || 500);
|
||||||
|
const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true';
|
||||||
|
|
||||||
const EMAILS = {
|
const EMAILS = {
|
||||||
DD_ADMIN: 'lince@gmail.com',
|
DD_ADMIN: 'lince@gmail.com',
|
||||||
@ -44,11 +49,14 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(email) {
|
async function login(email) {
|
||||||
|
if (!login.cache) login.cache = {};
|
||||||
|
if (login.cache[email]) return login.cache[email];
|
||||||
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
|
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
|
||||||
return data.token;
|
login.cache[email] = data.token;
|
||||||
|
return login.cache[email];
|
||||||
}
|
}
|
||||||
|
|
||||||
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms));
|
const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
|
||||||
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
|
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
@ -63,19 +71,26 @@ async function run() {
|
|||||||
|
|
||||||
console.log(`Targeting Dealer: ${targetDealer.legalName} (${targetDealer.id})`);
|
console.log(`Targeting Dealer: ${targetDealer.legalName} (${targetDealer.id})`);
|
||||||
|
|
||||||
// STEP 1: Submission (ASM)
|
let terminationId = args.terminationId;
|
||||||
console.log('[STEP 1] ASM Initiating Termination...');
|
if (!terminationId) {
|
||||||
const asmToken = await login(EMAILS.ASM);
|
console.log('[STEP 1] ASM Initiating Termination...');
|
||||||
const createRes = await apiRequest('/termination', 'POST', {
|
const asmToken = await login(EMAILS.ASM);
|
||||||
dealerId: targetDealer.id,
|
const createRes = await apiRequest('/termination', 'POST', {
|
||||||
category: 'Performance',
|
dealerId: targetDealer.id,
|
||||||
reason: 'Consistently failed to meet commitment targets.',
|
category: args.category || 'Performance',
|
||||||
proposedLwd: new Date().toISOString(),
|
reason: args.reason || 'Consistently failed to meet commitment targets.',
|
||||||
comments: 'Auto termination for testing flow ending with Legal Team, CCO, CEO.'
|
proposedLwd: new Date().toISOString(),
|
||||||
}, asmToken);
|
comments: 'Auto termination for testing flow ending with Legal Team, CCO, CEO.'
|
||||||
|
}, asmToken);
|
||||||
|
terminationId = createRes.termination.id;
|
||||||
|
console.log(`[STEP 1] Termination Request Created. ID: ${terminationId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[STEP 1] Resuming existing termination: ${terminationId}`);
|
||||||
|
}
|
||||||
|
|
||||||
const terminationId = createRes.termination.id;
|
const currentTermination = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken);
|
||||||
console.log(`[STEP 1] Termination Request Created. ID: ${terminationId}`);
|
const currentStage = currentTermination?.termination?.currentStage;
|
||||||
|
console.log(`[INFO] Current stage before progression: ${currentStage}`);
|
||||||
|
|
||||||
const approvals = [
|
const approvals = [
|
||||||
{ name: 'RBM Review', email: EMAILS.RBM, remarks: 'Performance concerns validated on-ground. Proceed with termination.' },
|
{ name: 'RBM Review', email: EMAILS.RBM, remarks: 'Performance concerns validated on-ground. Proceed with termination.' },
|
||||||
@ -94,8 +109,17 @@ async function run() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
let currentStep = 2;
|
const stageOrder = [
|
||||||
for (const actor of approvals) {
|
'Submitted', 'RBM Review', 'ZBH Review', 'DD Lead Review', 'Legal Verification', 'DD Head Review',
|
||||||
|
'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval',
|
||||||
|
'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'
|
||||||
|
];
|
||||||
|
const currentIndex = Math.max(0, stageOrder.indexOf(currentStage));
|
||||||
|
const currentStepStart = 2 + currentIndex;
|
||||||
|
let currentStep = currentStepStart;
|
||||||
|
|
||||||
|
for (let i = currentIndex; i < approvals.length; i++) {
|
||||||
|
const actor = approvals[i];
|
||||||
log(currentStep, `${actor.name} (${actor.email}) processing approval...`);
|
log(currentStep, `${actor.name} (${actor.email}) processing approval...`);
|
||||||
const token = await login(actor.email);
|
const token = await login(actor.email);
|
||||||
await apiRequest(`/termination/${terminationId}/status`, 'PUT', {
|
await apiRequest(`/termination/${terminationId}/status`, 'PUT', {
|
||||||
@ -108,13 +132,15 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) ---
|
// --- NEW: F&F CLEARANCE LOOP (16 DEPARTMENTS) ---
|
||||||
log(13, 'Starting 16-Department F&F Clearance Flow for Termination...');
|
if (!SHOULD_SKIP_CLEARANCES) {
|
||||||
|
log(13, 'Starting 16-Department F&F Clearance Flow for Termination...');
|
||||||
|
}
|
||||||
const terminationData = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken);
|
const terminationData = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken);
|
||||||
const fnfId = terminationData.termination.fnfSettlement?.id;
|
const fnfId = terminationData.termination.fnfSettlement?.id;
|
||||||
|
|
||||||
if (!fnfId) {
|
if (!fnfId) {
|
||||||
log('SKIP', 'FnF Settlement not initialized for this termination case.');
|
log('SKIP', 'FnF Settlement not initialized for this termination case.');
|
||||||
} else {
|
} else if (!SHOULD_SKIP_CLEARANCES) {
|
||||||
const departments = [
|
const departments = [
|
||||||
{ name: 'Warranty Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'No pending claims.' },
|
{ name: 'Warranty Department', status: 'Cleared', amount: 0, type: 'Recovery', remarks: 'No pending claims.' },
|
||||||
{ name: 'Accessories Department', status: 'Dues', amount: 15000, type: 'Recovery', remarks: 'Shortage in accessory stock.' },
|
{ name: 'Accessories Department', status: 'Dues', amount: 15000, type: 'Recovery', remarks: 'Shortage in accessory stock.' },
|
||||||
|
|||||||
@ -4,9 +4,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
const BASE_URL = 'http://localhost:5000/api';
|
const args = Object.fromEntries(
|
||||||
|
process.argv.slice(2)
|
||||||
|
.map(arg => arg.replace(/^--/, '').split('='))
|
||||||
|
.map(([k, v]) => [k, v ?? 'true'])
|
||||||
|
);
|
||||||
|
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
|
||||||
const PASSWORD = 'Admin@123';
|
const PASSWORD = 'Admin@123';
|
||||||
const OTP = '123456';
|
const OTP = '123456';
|
||||||
|
const STEP_DELAY_MS = Number(args.delayMs || 1000);
|
||||||
|
|
||||||
// Append timestamp to email to avoid duplicate application error
|
// Append timestamp to email to avoid duplicate application error
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
@ -79,7 +85,7 @@ let loaRequestId = null;
|
|||||||
/**
|
/**
|
||||||
* HELPERS
|
* HELPERS
|
||||||
*/
|
*/
|
||||||
const delay = (ms = 5000) => new Promise(res => setTimeout(res, ms));
|
const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
|
||||||
|
|
||||||
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
|
const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`);
|
||||||
|
|
||||||
@ -102,8 +108,11 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(email) {
|
async function login(email) {
|
||||||
|
if (!login.cache) login.cache = {};
|
||||||
|
if (login.cache[email]) return login.cache[email];
|
||||||
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
|
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
|
||||||
return data.token; // Standard login returns token at root
|
login.cache[email] = data.token;
|
||||||
|
return login.cache[email]; // Standard login returns token at root
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prospectLogin(phone) {
|
async function prospectLogin(phone) {
|
||||||
@ -131,19 +140,47 @@ async function mockUploadDocument(appId, token, docType) {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getApplicationStatus(appId, token) {
|
||||||
|
const res = await apiRequest(`/onboarding/applications/${appId}`, 'GET', null, token);
|
||||||
|
return res?.data?.overallStatus || res?.data?.status || 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureMandatoryCodeGenFields(appId, token) {
|
||||||
|
const details = await apiRequest(`/onboarding/applications/${appId}`, 'GET', null, token);
|
||||||
|
const app = details?.data || {};
|
||||||
|
|
||||||
|
const fallbackFields = {
|
||||||
|
panNumber: app.panNumber || 'ABCDE1234F',
|
||||||
|
gstNumber: app.gstNumber || '07ABCDE1234F1Z5',
|
||||||
|
bankName: app.bankName || 'HDFC Bank',
|
||||||
|
accountNumber: app.accountNumber || '50100223344556',
|
||||||
|
ifscCode: app.ifscCode || 'HDFC0001234',
|
||||||
|
accountHolderName: app.accountHolderName || 'Kumar Automobiles Private Limited',
|
||||||
|
registeredAddress: app.registeredAddress || '123, Main Road, New Delhi'
|
||||||
|
};
|
||||||
|
|
||||||
|
await apiRequest(`/onboarding/applications/${appId}`, 'PUT', fallbackFields, token);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MAIN WORKFLOW
|
* MAIN WORKFLOW
|
||||||
*/
|
*/
|
||||||
async function triggerWorkflow() {
|
async function triggerWorkflow() {
|
||||||
console.log('--- STARTING DEALER ONBOARDING E2E FLOW ---\n');
|
console.log('--- STARTING DEALER ONBOARDING E2E FLOW ---\n');
|
||||||
|
|
||||||
// 1. PUBLIC APPLY
|
if (args.applicationId) {
|
||||||
log(1, 'Public Prospect Application Submission...');
|
applicationId = args.applicationId;
|
||||||
const appResponse = await apiRequest('/onboarding/apply', 'POST', PROSPECT_PAYLOAD);
|
applicationUUID = args.applicationId;
|
||||||
applicationId = appResponse.data.applicationId;
|
log(1, `Resuming with existing application: ${applicationUUID}`);
|
||||||
applicationUUID = appResponse.data.id;
|
} else {
|
||||||
log(1, `Application Created: ${applicationId} (UUID: ${applicationUUID})`);
|
// 1. PUBLIC APPLY
|
||||||
await delay();
|
log(1, 'Public Prospect Application Submission...');
|
||||||
|
const appResponse = await apiRequest('/onboarding/apply', 'POST', PROSPECT_PAYLOAD);
|
||||||
|
applicationId = appResponse.data.applicationId;
|
||||||
|
applicationUUID = appResponse.data.id;
|
||||||
|
log(1, `Application Created: ${applicationId} (UUID: ${applicationUUID})`);
|
||||||
|
await delay();
|
||||||
|
}
|
||||||
|
|
||||||
// 2. ADMIN SHORTLIST
|
// 2. ADMIN SHORTLIST
|
||||||
log(2, 'Admin Login & Shortlisting...');
|
log(2, 'Admin Login & Shortlisting...');
|
||||||
@ -371,14 +408,8 @@ async function triggerWorkflow() {
|
|||||||
log(7.5, 'LOI Milestone Complete.');
|
log(7.5, 'LOI Milestone Complete.');
|
||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 8. GENERATE DEALER CODES (Sequence: Post-LOI, Pre-LOA)
|
// 8. PAYMENT GATE (SECURITY DEPOSIT FIRST AS PER CURRENT FLOW)
|
||||||
log(8, 'Admin Generating SAP Dealer Codes...');
|
log(8, 'Finance Verifying SECURITY_DEPOSIT to unlock LOI Issued...');
|
||||||
await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
|
|
||||||
log(8, 'Dealer Codes Generated.');
|
|
||||||
await delay();
|
|
||||||
|
|
||||||
// 9. PAYMENT GATE
|
|
||||||
log(9, 'Prospect Uploading Payment Receipt (Mock)...');
|
|
||||||
const financeToken = await login(EMAILS.FINANCE);
|
const financeToken = await login(EMAILS.FINANCE);
|
||||||
await apiRequest('/loa/security-deposit', 'POST', {
|
await apiRequest('/loa/security-deposit', 'POST', {
|
||||||
applicationId: applicationUUID,
|
applicationId: applicationUUID,
|
||||||
@ -387,9 +418,37 @@ async function triggerWorkflow() {
|
|||||||
depositType: 'SECURITY_DEPOSIT',
|
depositType: 'SECURITY_DEPOSIT',
|
||||||
status: 'Verified'
|
status: 'Verified'
|
||||||
}, financeToken);
|
}, financeToken);
|
||||||
log(9, 'Security Deposit Verified.');
|
log(8, 'Security Deposit Verified.');
|
||||||
|
await delay();
|
||||||
|
|
||||||
log(9.1, 'Finance Verifying FIRST FILL (₹15L)...');
|
// 9. GENERATE DEALER CODES (NOW RETRY-SAFE WITH STATUS CHECK)
|
||||||
|
let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||||
|
log(9, `Current status before code generation: ${statusBeforeCodeGen}`);
|
||||||
|
log(9, 'Ensuring mandatory PAN/GST/Bank fields before code generation...');
|
||||||
|
await ensureMandatoryCodeGenFields(applicationUUID, adminToken);
|
||||||
|
await delay(300);
|
||||||
|
|
||||||
|
if (statusBeforeCodeGen === 'Security Details') {
|
||||||
|
log(9, 'Status is Security Details; re-verifying Security Deposit to move to LOI Issued...');
|
||||||
|
await apiRequest('/loa/security-deposit', 'POST', {
|
||||||
|
applicationId: applicationUUID,
|
||||||
|
amount: 500000,
|
||||||
|
paymentReference: `PAY-RETRY-${Date.now()}`,
|
||||||
|
depositType: 'SECURITY_DEPOSIT',
|
||||||
|
status: 'Verified'
|
||||||
|
}, financeToken);
|
||||||
|
await delay();
|
||||||
|
statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||||
|
log(9, `Status after re-verify: ${statusBeforeCodeGen}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(9, 'Admin Generating SAP Dealer Codes...');
|
||||||
|
await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
|
||||||
|
log(9, 'Dealer Codes Generated.');
|
||||||
|
await delay();
|
||||||
|
|
||||||
|
// 10. FIRST FILL (POST CODE-GENERATION)
|
||||||
|
log(10, 'Finance Verifying FIRST FILL (₹15L)...');
|
||||||
await apiRequest('/loa/security-deposit', 'POST', {
|
await apiRequest('/loa/security-deposit', 'POST', {
|
||||||
applicationId: applicationUUID,
|
applicationId: applicationUUID,
|
||||||
amount: 1500000,
|
amount: 1500000,
|
||||||
@ -397,11 +456,11 @@ async function triggerWorkflow() {
|
|||||||
depositType: 'FIRST_FILL',
|
depositType: 'FIRST_FILL',
|
||||||
status: 'Verified'
|
status: 'Verified'
|
||||||
}, financeToken);
|
}, financeToken);
|
||||||
log(9.1, 'Final Security Deposit Verified.');
|
log(10, 'Final Security Deposit Verified.');
|
||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 9.2 ADMIN UPDATING STATUTORY & BANK DETAILS
|
// 11. ADMIN UPDATING STATUTORY & BANK DETAILS
|
||||||
log(9.2, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
|
log(11, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
|
||||||
await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', {
|
await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', {
|
||||||
accountHolderName: 'Ramesh Automobiles Private Limited',
|
accountHolderName: 'Ramesh Automobiles Private Limited',
|
||||||
panNumber: 'ABCDE1234F',
|
panNumber: 'ABCDE1234F',
|
||||||
@ -410,11 +469,11 @@ async function triggerWorkflow() {
|
|||||||
accountNumber: '50100223344556',
|
accountNumber: '50100223344556',
|
||||||
ifscCode: 'HDFC0001234'
|
ifscCode: 'HDFC0001234'
|
||||||
}, adminToken);
|
}, adminToken);
|
||||||
log(9.2, 'Statutory & Bank details updated.');
|
log(11, 'Statutory & Bank details updated.');
|
||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 10. FINAL LOA APPROVAL
|
// 12. FINAL LOA APPROVAL
|
||||||
log(10, 'NBH & Head Approving Final LOA...');
|
log(12, 'NBH & Head Approving Final LOA...');
|
||||||
const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
|
const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
|
||||||
const finalLoaRequestId = loaRes.data.id;
|
const finalLoaRequestId = loaRes.data.id;
|
||||||
|
|
||||||
@ -427,16 +486,16 @@ async function triggerWorkflow() {
|
|||||||
action: 'Approved',
|
action: 'Approved',
|
||||||
remarks: 'NBH Approval (Level 2)'
|
remarks: 'NBH Approval (Level 2)'
|
||||||
}, nbhToken);
|
}, nbhToken);
|
||||||
log(10, 'LOA Fully Approved.');
|
log(12, 'LOA Fully Approved.');
|
||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 11. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
|
// 13. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
|
||||||
log(11, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
|
log(13, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
|
||||||
const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken);
|
const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||||
const checklistId = eorInit.data.id;
|
const checklistId = eorInit.data.id;
|
||||||
log(11, `EOR Checklist Created (ID: ${checklistId})`);
|
log(13, `EOR Checklist Created (ID: ${checklistId})`);
|
||||||
|
|
||||||
log(11.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...');
|
log(13.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...');
|
||||||
const eorItems = [
|
const eorItems = [
|
||||||
{ itemType: 'Sales', description: 'Sales Standards' },
|
{ itemType: 'Sales', description: 'Sales Standards' },
|
||||||
{ itemType: 'Service', description: 'Service & Spares' },
|
{ itemType: 'Service', description: 'Service & Spares' },
|
||||||
@ -460,9 +519,9 @@ async function triggerWorkflow() {
|
|||||||
remarks: 'Verified by Auditor - Compliant'
|
remarks: 'Verified by Auditor - Compliant'
|
||||||
}, adminToken);
|
}, adminToken);
|
||||||
}
|
}
|
||||||
console.log('\n[STEP 11.1] All EOR items marked as compliant.');
|
console.log('\n[STEP 13.1] All EOR items marked as compliant.');
|
||||||
|
|
||||||
log(11.2, 'Auditor Submitting Final EOR Audit...');
|
log(13.2, 'Auditor Submitting Final EOR Audit...');
|
||||||
await apiRequest(`/eor/audit/${checklistId}`, 'POST', {
|
await apiRequest(`/eor/audit/${checklistId}`, 'POST', {
|
||||||
status: 'Completed',
|
status: 'Completed',
|
||||||
overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
|
overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
|
||||||
@ -470,32 +529,32 @@ async function triggerWorkflow() {
|
|||||||
|
|
||||||
// Status check
|
// Status check
|
||||||
const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
|
const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
|
||||||
log(11.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
|
log(13.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
|
||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 12. FINAL ONBOARDING
|
// 14. FINAL ONBOARDING
|
||||||
log(12, 'Admin Finalizing Dealer Onboarding...');
|
log(14, 'Admin Finalizing Dealer Onboarding...');
|
||||||
await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
|
await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 13. VERIFICATION
|
// 15. VERIFICATION
|
||||||
log(13, 'Verifying Dealer Record Creation...');
|
log(15, 'Verifying Dealer Record Creation...');
|
||||||
const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken);
|
const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken);
|
||||||
if (!dealerRes.success || !dealerRes.data) {
|
if (!dealerRes.success || !dealerRes.data) {
|
||||||
throw new Error('Verification Failed: Dealer record not found after onboarding.');
|
throw new Error('Verification Failed: Dealer record not found after onboarding.');
|
||||||
}
|
}
|
||||||
log(13, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`);
|
log(15, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`);
|
||||||
|
|
||||||
log(13.1, 'Verifying User Account Role Update...');
|
log(15.1, 'Verifying User Account Role Update...');
|
||||||
const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken);
|
const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken);
|
||||||
const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL);
|
const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL);
|
||||||
if (!dealerUser || dealerUser.roleCode !== 'Dealer') {
|
if (!dealerUser || dealerUser.roleCode !== 'Dealer') {
|
||||||
throw new Error(`Verification Failed: User role not updated to 'Dealer'. Current role: ${dealerUser?.roleCode}`);
|
throw new Error(`Verification Failed: User role not updated to 'Dealer'. Current role: ${dealerUser?.roleCode}`);
|
||||||
}
|
}
|
||||||
log(13.1, `User role confirmed: ${dealerUser.roleCode}`);
|
log(15.1, `User role confirmed: ${dealerUser.roleCode}`);
|
||||||
|
|
||||||
log(13.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
|
log(15.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
|
||||||
log(13.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
|
log(15.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user