end to end flow testing for all modules paralley enhancing profile schema to gather all info

This commit is contained in:
laxman h 2026-04-10 20:54:59 +05:30
parent 934fd7a907
commit 19c766c999
23 changed files with 385 additions and 146 deletions

View File

@ -1,22 +1,41 @@
import pkg from 'pg';
const { Client } = pkg;
import 'dotenv/config'; import 'dotenv/config';
import db from './src/database/models/index.js';
const { RequestParticipant } = (db as any).default || db;
const applicationId = '6139d6f9-f3c1-4e55-903b-3516d3a08955'; async function check() {
const client = new Client({
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'Admin@123',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'royal_enfield_onboarding',
port: parseInt(process.env.DB_PORT || '5432'),
});
async function checkParticipants() {
try { try {
const count = await RequestParticipant.count({ await client.connect();
where: { requestId: applicationId, requestType: 'application' } const res = await client.query('SELECT * FROM request_participants WHERE "requestType" = $1', ['resignation']);
}); console.log(`Found ${res.rows.length} participants for Resignations.`);
const termRes = await client.query('SELECT * FROM request_participants WHERE "requestType" = $1', ['termination']);
console.log(`Found ${termRes.rows.length} participants for Terminations.`);
console.log(`Application has ${count} participants.`); if (res.rows.length > 0) {
console.log('Sample Resignation Participant:', JSON.stringify(res.rows[0], null, 2));
}
if (termRes.rows.length > 0) {
console.log('Sample Termination Participant:', JSON.stringify(termRes.rows[0], null, 2));
}
} catch (error) { const resignations = await client.query('SELECT id, "resignationId" FROM resignations LIMIT 5');
console.error('Error checking participants:', error); console.log('Resignations in DB:', resignations.rows.map(r => r.resignationId));
} catch (err) {
console.error('Error:', err.message);
} finally { } finally {
process.exit(); await client.end();
} }
} }
checkParticipants(); check();

View File

@ -1,32 +0,0 @@
import { ParticipantService } from './src/services/ParticipantService.js';
async function run() {
try {
const requestId = '29b742a7-6d9f-4736-8aae-295ffe32ef75';
console.log(`Fixing participants for resignation ${requestId}...`);
const { Resignation, User, Dealer, Application, District } = (await import('./src/database/models/index.js')).default;
const resignation = await Resignation.findByPk(requestId);
console.log('Resignation Record:', JSON.stringify(resignation, null, 2));
if (resignation) {
const user = await User.findByPk(resignation.dealerId);
console.log('User Record:', JSON.stringify(user, null, 2));
if (user && user.dealerId) {
const dealer = await Dealer.findByPk(user.dealerId, {
include: [{ model: Application, as: 'application', include: [{ model: District, as: 'district' }] }]
});
console.log('Dealer/Application/District Record:', JSON.stringify(dealer, null, 2));
}
}
await ParticipantService.assignResignationParticipants(requestId);
console.log('Done.');
process.exit(0);
} catch (error) {
console.error('Error fixing participants:', error);
process.exit(1);
}
}
run();

View File

@ -271,7 +271,8 @@ export default (sequelize: Sequelize) => {
Application.hasMany(models.RequestParticipant, { Application.hasMany(models.RequestParticipant, {
foreignKey: 'requestId', foreignKey: 'requestId',
as: 'participants', as: 'participants',
scope: { requestType: 'application' } scope: { requestType: 'application' },
constraints: false
}); });
Application.hasOne(models.DealerCode, { foreignKey: 'applicationId', as: 'dealerCode' }); Application.hasOne(models.DealerCode, { foreignKey: 'applicationId', as: 'dealerCode' });
Application.hasMany(models.StageApprovalAction, { foreignKey: 'applicationId', as: 'stageApprovals' }); Application.hasMany(models.StageApprovalAction, { foreignKey: 'applicationId', as: 'stageApprovals' });
@ -279,6 +280,8 @@ export default (sequelize: Sequelize) => {
Application.hasMany(models.SecurityDeposit, { foreignKey: 'applicationId', as: 'securityDeposits' }); Application.hasMany(models.SecurityDeposit, { foreignKey: 'applicationId', as: 'securityDeposits' });
Application.hasOne(models.EorChecklist, { foreignKey: 'applicationId', as: 'eorChecklist' }); Application.hasOne(models.EorChecklist, { foreignKey: 'applicationId', as: 'eorChecklist' });
Application.hasMany(models.FddAssignment, { foreignKey: 'applicationId', as: 'fddAssignments' }); Application.hasMany(models.FddAssignment, { foreignKey: 'applicationId', as: 'fddAssignments' });
Application.hasMany(models.LoiRequest, { foreignKey: 'applicationId', as: 'loiRequests' });
Application.hasMany(models.LoaRequest, { foreignKey: 'applicationId', as: 'loaRequests' });
}; };
return Application; return Application;

View File

@ -2,7 +2,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface DealerAttributes { export interface DealerAttributes {
id: string; id: string;
applicationId: string; applicationId: string | null;
dealerCodeId: string | null; dealerCodeId: string | null;
legalName: string; legalName: string;
businessName: string; businessName: string;
@ -12,6 +12,13 @@ export interface DealerAttributes {
panNumber: string | null; panNumber: string | null;
status: string; status: string;
onboardedAt: Date | null; onboardedAt: Date | null;
loiDate: Date | null;
loaDate: Date | null;
isLegacy: boolean;
securityDepositAmount: number | null;
securityDepositDate: Date | null;
lastWorkingDay: Date | null;
exitReason: string | null;
} }
export interface DealerInstance extends Model<DealerAttributes>, DealerAttributes { } export interface DealerInstance extends Model<DealerAttributes>, DealerAttributes { }
@ -25,7 +32,7 @@ export default (sequelize: Sequelize) => {
}, },
applicationId: { applicationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: true,
references: { references: {
model: 'applications', model: 'applications',
key: 'id' key: 'id'
@ -70,6 +77,34 @@ export default (sequelize: Sequelize) => {
onboardedAt: { onboardedAt: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true
},
loiDate: {
type: DataTypes.DATE,
allowNull: true
},
loaDate: {
type: DataTypes.DATE,
allowNull: true
},
isLegacy: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
securityDepositAmount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: true
},
securityDepositDate: {
type: DataTypes.DATE,
allowNull: true
},
lastWorkingDay: {
type: DataTypes.DATE,
allowNull: true
},
exitReason: {
type: DataTypes.TEXT,
allowNull: true
} }
}, { }, {
tableName: 'dealers', tableName: 'dealers',

View File

@ -8,6 +8,7 @@ export interface FddReportAttributes {
recommendation: string | null; recommendation: string | null;
verifiedAt: Date | null; verifiedAt: Date | null;
verifiedBy: string | null; verifiedBy: string | null;
submittedBy: string | null;
} }
export interface FddReportInstance extends Model<FddReportAttributes>, FddReportAttributes { } export interface FddReportInstance extends Model<FddReportAttributes>, FddReportAttributes { }
@ -54,6 +55,14 @@ export default (sequelize: Sequelize) => {
model: 'users', model: 'users',
key: 'id' key: 'id'
} }
},
submittedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
} }
}, { }, {
tableName: 'fdd_reports', tableName: 'fdd_reports',
@ -64,6 +73,7 @@ export default (sequelize: Sequelize) => {
FddReport.belongsTo(models.FddAssignment, { foreignKey: 'assignmentId', as: 'assignment' }); FddReport.belongsTo(models.FddAssignment, { foreignKey: 'assignmentId', as: 'assignment' });
FddReport.belongsTo(models.OnboardingDocument, { foreignKey: 'reportDocumentId', as: 'reportDocument' }); FddReport.belongsTo(models.OnboardingDocument, { foreignKey: 'reportDocumentId', as: 'reportDocument' });
FddReport.belongsTo(models.User, { foreignKey: 'verifiedBy', as: 'verifier' }); FddReport.belongsTo(models.User, { foreignKey: 'verifiedBy', as: 'verifier' });
FddReport.belongsTo(models.User, { foreignKey: 'submittedBy', as: 'submitter' });
}; };
return FddReport; return FddReport;

View File

@ -2,7 +2,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface LoaRequestAttributes { export interface LoaRequestAttributes {
id: string; id: string;
applicationId: string; applicationId: string | null;
status: string; status: string;
requestedBy: string | null; requestedBy: string | null;
approvedAt: Date | null; approvedAt: Date | null;
@ -20,7 +20,7 @@ export default (sequelize: Sequelize) => {
}, },
applicationId: { applicationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: true,
references: { references: {
model: 'applications', model: 'applications',
key: 'id' key: 'id'

View File

@ -2,7 +2,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface LoiRequestAttributes { export interface LoiRequestAttributes {
id: string; id: string;
applicationId: string; applicationId: string | null;
status: string; status: string;
requestedBy: string | null; requestedBy: string | null;
approvedAt: Date | null; approvedAt: Date | null;
@ -20,7 +20,7 @@ export default (sequelize: Sequelize) => {
}, },
applicationId: { applicationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: true,
references: { references: {
model: 'applications', model: 'applications',
key: 'id' key: 'id'

View File

@ -150,7 +150,8 @@ export default (sequelize: Sequelize) => {
Resignation.hasMany(models.RequestParticipant, { Resignation.hasMany(models.RequestParticipant, {
foreignKey: 'requestId', foreignKey: 'requestId',
as: 'participants', as: 'participants',
scope: { requestType: 'resignation' } scope: { requestType: 'resignation' },
constraints: false
}); });
}; };

View File

@ -2,12 +2,12 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface SecurityDepositAttributes { export interface SecurityDepositAttributes {
id: string; id: string;
applicationId: string; applicationId: string | null;
amount: number; amount: number;
paymentReference: string | null; paymentReference: string | null;
proofDocumentId: string | null; proofDocumentId: string | null;
status: string; status: string;
depositType: 'INITIAL' | 'FINAL'; depositType: 'SECURITY_DEPOSIT' | 'FIRST_FILL';
verifiedAt: Date | null; verifiedAt: Date | null;
verifiedBy: string | null; verifiedBy: string | null;
} }
@ -23,7 +23,7 @@ export default (sequelize: Sequelize) => {
}, },
applicationId: { applicationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: true,
references: { references: {
model: 'applications', model: 'applications',
key: 'id' key: 'id'
@ -50,9 +50,9 @@ export default (sequelize: Sequelize) => {
defaultValue: 'pending' defaultValue: 'pending'
}, },
depositType: { depositType: {
type: DataTypes.ENUM('INITIAL', 'FINAL'), type: DataTypes.ENUM('SECURITY_DEPOSIT', 'FIRST_FILL'),
allowNull: false, allowNull: false,
defaultValue: 'INITIAL' defaultValue: 'SECURITY_DEPOSIT'
}, },
verifiedAt: { verifiedAt: {
type: DataTypes.DATE, type: DataTypes.DATE,

View File

@ -114,7 +114,8 @@ export default (sequelize: Sequelize) => {
TerminationRequest.hasMany(models.RequestParticipant, { TerminationRequest.hasMany(models.RequestParticipant, {
foreignKey: 'requestId', foreignKey: 'requestId',
as: 'participants', as: 'participants',
scope: { requestType: 'termination' } scope: { requestType: 'termination' },
constraints: false
}); });
}; };

View File

@ -150,14 +150,24 @@ const processStageDecision = async (params: {
} }
} }
await db.FddReport.create({ let report = await db.FddReport.findOne({ where: { assignmentId: assignment.id } });
assignmentId: assignment.id,
reportDocumentId: lastReportDoc?.id || null, if (report) {
findings, await report.update({
recommendation, verifiedAt: new Date(),
verifiedAt: new Date(), verifiedBy: userId
verifiedBy: userId });
}); } else {
await db.FddReport.create({
assignmentId: assignment.id,
reportDocumentId: lastReportDoc?.id || null,
findings,
recommendation,
verifiedAt: new Date(),
verifiedBy: userId,
submittedBy: userId // Admin submitted it if no existing report
});
}
await assignment.update({ status: 'Report Submitted' }); await assignment.update({ status: 'Report Submitted' });

View File

@ -97,6 +97,24 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
}, { where: { id: targetDealerCodeId } }); }, { where: { id: targetDealerCodeId } });
} }
// Fetch milestone dates and payments from requests
const loiRequest = await db.LoiRequest.findOne({
where: { applicationId: application.id, status: { [Op.iLike]: 'Approved' } },
order: [['approvedAt', 'DESC']]
});
const loaRequest = await db.LoaRequest.findOne({
where: { applicationId: application.id, status: { [Op.iLike]: 'Approved' } },
order: [['approvedAt', 'DESC']]
});
const deposit = await db.SecurityDeposit.findOne({
where: {
applicationId: application.id,
status: 'Verified',
depositType: 'SECURITY_DEPOSIT' // Strictly the Security Deposit
},
order: [['verifiedAt', 'DESC']]
});
// Create Dealer Profile // Create Dealer Profile
dealer = await Dealer.create({ dealer = await Dealer.create({
applicationId, applicationId,
@ -105,7 +123,11 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
businessName: application.applicantName, businessName: application.applicantName,
constitutionType: application.constitutionType || 'Proprietorship', constitutionType: application.constitutionType || 'Proprietorship',
status: 'Active', status: 'Active',
onboardedAt: new Date() onboardedAt: new Date(),
loiDate: loiRequest?.approvedAt,
loaDate: loaRequest?.approvedAt,
securityDepositAmount: deposit?.amount,
securityDepositDate: deposit?.verifiedAt
}); });
await AuditLog.create({ await AuditLog.create({

View File

@ -23,7 +23,14 @@ export const getAssignment = async (req: Request, res: Response) => {
const assignment = await FddAssignment.findOne({ const assignment = await FddAssignment.findOne({
where: { applicationId: application.id }, where: { applicationId: application.id },
include: [{ model: FddReport, as: 'reports' }] include: [{
model: FddReport,
as: 'reports',
include: [
{ model: db.User, as: 'submitter', attributes: ['fullName'] },
{ model: db.User, as: 'verifier', attributes: ['fullName'] }
]
}]
}); });
res.json({ success: true, data: assignment }); res.json({ success: true, data: assignment });
} catch (error) { } catch (error) {
@ -76,25 +83,60 @@ export const assignAgency = async (req: AuthRequest, res: Response) => {
export const uploadReport = async (req: AuthRequest, res: Response) => { export const uploadReport = async (req: AuthRequest, res: Response) => {
try { try {
const { assignmentId, reportDocumentId, findings, recommendation } = req.body; const { assignmentId, reportDocumentId, findings, recommendation, applicationId } = req.body;
let finalAssignmentId = assignmentId;
// Auto-assign logic if assignmentId is missing
if (!finalAssignmentId && applicationId) {
const appId = applicationId;
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(appId);
const application = await Application.findOne({
where: isUUID ? { [Op.or]: [{ id: appId }, { applicationId: appId }] } : { applicationId: appId }
});
const report = await FddReport.create({ if (application) {
assignmentId, const [newAssignment] = await FddAssignment.findOrCreate({
reportDocumentId, where: { applicationId: application.id },
findings, defaults: {
recommendation, assignedToAgency: req.user?.id,
verifiedAt: new Date(), status: 'Assigned'
verifiedBy: req.user?.id // Auto-verified by uploader for now? Or separate verify step? }
}); });
finalAssignmentId = newAssignment.id;
}
}
if (!finalAssignmentId) {
return res.status(400).json({ success: false, message: 'Assignment ID or valid Application ID is required' });
}
let report = await FddReport.findOne({ where: { assignmentId: finalAssignmentId } });
if (report) {
await report.update({
reportDocumentId,
findings,
recommendation,
submittedBy: req.user?.id
});
} else {
report = await FddReport.create({
assignmentId: finalAssignmentId,
reportDocumentId,
findings,
recommendation,
submittedBy: req.user?.id
});
}
// Update Assignment Status // Update Assignment Status
await FddAssignment.update( await FddAssignment.update(
{ status: 'Report Submitted' }, { status: 'Report Submitted' },
{ where: { id: assignmentId } } { where: { id: finalAssignmentId } }
); );
// Fetch application to transition // Fetch application to transition
const assignment = await FddAssignment.findByPk(assignmentId); const assignment = await FddAssignment.findByPk(finalAssignmentId);
if (assignment) { if (assignment) {
const application = await Application.findByPk(assignment.applicationId); const application = await Application.findByPk(assignment.applicationId);
if (application) { if (application) {

View File

@ -124,7 +124,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
const finalDeposit = await SecurityDeposit.findOne({ const finalDeposit = await SecurityDeposit.findOne({
where: { where: {
applicationId: request.applicationId, applicationId: request.applicationId,
depositType: 'FINAL', depositType: 'FIRST_FILL',
status: 'Verified' status: 'Verified'
} }
}); });
@ -132,7 +132,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
if (!finalDeposit) { if (!finalDeposit) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: `LOA Approval Blocked: The Final Security Deposit (₹15L) is either Pending or not found. Finance team must verify the payment before proceeding.` message: `LOA Approval Blocked: The First Fill (₹15L) is either Pending or not found. Finance team must verify the payment before proceeding.`
}); });
} }
} }
@ -312,7 +312,7 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) =>
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' });
let deposit = await SecurityDeposit.findOne({ let deposit = await SecurityDeposit.findOne({
where: { applicationId: application.id, depositType: depositType || 'INITIAL' } where: { applicationId: application.id, depositType: depositType || 'SECURITY_DEPOSIT' }
}); });
if (deposit) { if (deposit) {
@ -328,7 +328,7 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) =>
paymentReference, paymentReference,
proofDocumentId, proofDocumentId,
status: status || 'Pending', status: status || 'Pending',
depositType: depositType || 'INITIAL', depositType: depositType || 'SECURITY_DEPOSIT',
verifiedBy: status === 'Verified' ? req.user?.id : null, verifiedBy: status === 'Verified' ? req.user?.id : null,
verifiedAt: status === 'Verified' ? new Date() : null verifiedAt: status === 'Verified' ? new Date() : null
}); });
@ -341,18 +341,18 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) =>
// --- AUTOMATION: After verification transitions --- // --- AUTOMATION: After verification transitions ---
// 1. If INITIAL Payment Verified -> Move to LOI Issue Stage // 1. If SECURITY_DEPOSIT Payment Verified -> Move to LOI Issue Stage
if ((depositType === 'INITIAL' || !depositType) && status === 'Verified') { if ((depositType === 'SECURITY_DEPOSIT' || !depositType) && status === 'Verified') {
console.log(`[DEBUG] Initial Security Deposit verified. Moving to LOI Issued stage...`); console.log(`[DEBUG] Security Deposit verified. Moving to LOI Issued stage...`);
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_ISSUED, req.user?.id || null, { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOI_ISSUED, req.user?.id || null, {
reason: 'Initial Security Deposit verified. Proceeding to LOI Issuance.', reason: 'Security Deposit verified. Proceeding to LOI Issuance.',
stage: APPLICATION_STAGES.LOI, stage: APPLICATION_STAGES.LOI,
progressPercentage: 80 progressPercentage: 80
}); });
} }
// 2. If FINAL Payment Verified -> Move to LOA Pending stage // 2. If FIRST_FILL Payment Verified -> Move to LOA Pending stage
if (depositType === 'FINAL' && status === 'Verified') { if (depositType === 'FIRST_FILL' && status === 'Verified') {
// Ensure LoaRequest exists for the next step // Ensure LoaRequest exists for the next step
await db.LoaRequest.findOrCreate({ await db.LoaRequest.findOrCreate({
where: { applicationId: application.id }, where: { applicationId: application.id },
@ -360,7 +360,7 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) =>
}); });
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOA_PENDING, req.user?.id || null, { await WorkflowService.transitionApplication(application, APPLICATION_STATUS.LOA_PENDING, req.user?.id || null, {
reason: 'Final Security Deposit Verified. Initiating LOA Approval stage.', reason: 'First Fill Verified. Initiating LOA Approval stage.',
progressPercentage: 90 progressPercentage: 90
}); });
} }

View File

@ -244,7 +244,7 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
// Create Initial Security Deposit record (Advance Payment) // Create Initial Security Deposit record (Advance Payment)
await db.SecurityDeposit.findOrCreate({ await db.SecurityDeposit.findOrCreate({
where: { applicationId: request.applicationId, depositType: 'INITIAL' }, where: { applicationId: request.applicationId, depositType: 'SECURITY_DEPOSIT' },
defaults: { defaults: {
amount: 200000, // 2 Lakhs Advance amount: 200000, // 2 Lakhs Advance
status: 'Pending' status: 'Pending'

View File

@ -870,7 +870,7 @@ export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
// Create Final Security Deposit record (Blocker for LOA) // Create Final Security Deposit record (Blocker for LOA)
await db.SecurityDeposit.findOrCreate({ await db.SecurityDeposit.findOrCreate({
where: { applicationId: application.id, depositType: 'FINAL' }, where: { applicationId: application.id, depositType: 'FIRST_FILL' },
defaults: { defaults: {
amount: 1500000, // 15 Lakhs Final amount: 1500000, // 15 Lakhs Final
status: 'Pending' status: 'Pending'

View File

@ -100,7 +100,29 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }, where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr },
include: [ include: [
{ model: Outlet, as: 'outlet' }, { model: Outlet, as: 'outlet' },
{ model: User, as: 'dealer', attributes: ['fullName', 'email'] }, {
model: User,
as: 'dealer',
attributes: ['id', 'fullName', 'email', 'roleCode'],
include: [
{
model: db.Dealer,
as: 'dealerProfile',
include: [
{ model: db.DealerCode, as: 'dealerCode' },
{
model: db.Application,
as: 'application',
include: [
{ model: db.District, as: 'district' },
{ model: db.LoiRequest, as: 'loiRequests', where: { status: 'approved' }, required: false },
{ model: db.LoaRequest, as: 'loaRequests', where: { status: 'approved' }, required: false }
]
}
]
}
]
},
{ model: Worknote, as: 'worknotes' }, { model: Worknote, as: 'worknotes' },
{ {
model: db.RequestParticipant, model: db.RequestParticipant,

View File

@ -120,7 +120,29 @@ export const getResignationById = async (req: AuthRequest, res: Response, next:
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
include: [ include: [
{ model: db.Outlet, as: 'outlet' }, { model: db.Outlet, as: 'outlet' },
{ model: db.User, as: 'dealer', attributes: ['id', 'fullName', 'email'] }, {
model: db.User,
as: 'dealer',
attributes: ['id', 'fullName', 'email', 'roleCode'],
include: [
{
model: db.Dealer,
as: 'dealerProfile',
include: [
{ model: db.DealerCode, as: 'dealerCode' },
{
model: db.Application,
as: 'application',
include: [
{ model: db.District, as: 'district' },
{ model: db.LoiRequest, as: 'loiRequests', where: { status: 'approved' }, required: false },
{ model: db.LoaRequest, as: 'loaRequests', where: { status: 'approved' }, required: false }
]
}
]
}
]
},
{ {
model: db.ResignationDocument, model: db.ResignationDocument,
as: 'uploadedDocuments', as: 'uploadedDocuments',

View File

@ -2,7 +2,7 @@ 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 } from '../../common/config/constants.js';
import { 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';
@ -94,20 +94,36 @@ export const getTerminations = async (req: AuthRequest, res: Response, next: Nex
export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => { export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
try { try {
const { id } = req.params; const { id } = req.params;
const termination = await db.TerminationRequest.findByPk(id, { 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 },
include: [ include: [
{ {
model: db.Dealer, model: db.Dealer,
as: 'dealer', as: 'dealer',
include: [ include: [
{ model: db.DealerCode, as: 'dealerCode' },
{ {
model: db.Application, model: db.Application,
as: 'application', as: 'application',
include: [ include: [
{ model: db.District, as: 'district' } { model: db.District, as: 'district' },
{
model: db.LoiRequest,
as: 'loiRequests',
where: { status: 'approved' },
required: false
},
{
model: db.LoaRequest,
as: 'loaRequests',
where: { status: 'approved' },
required: false
}
] ]
} },
{ model: db.DealerCode, as: 'dealerCode' }
] ]
}, },
{ model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'email'] }, { model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'email'] },

View File

@ -81,18 +81,27 @@ export class ParticipantService {
const termination = await TerminationRequest.findByPk(terminationId); const termination = await TerminationRequest.findByPk(terminationId);
if (!termination) return; if (!termination) return;
// TerminationRequest already uses Dealer ID
const managers = await this.getDealerLocationManagers(termination.dealerId);
const participantIds = new Set<string>(); const participantIds = new Set<string>();
// 0. The Dealer (Requester) should be a participant
if (termination.dealerId) {
// Find user account for this dealer
const dealerUser = await User.findOne({ where: { dealerId: termination.dealerId } });
if (dealerUser) participantIds.add(dealerUser.id);
}
// The Initiator (Admin who started termination)
if (termination.initiatedBy) participantIds.add(termination.initiatedBy);
// 1. Location based managers // 1. Location based managers
const managers = await this.getDealerLocationManagers(termination.dealerId);
if (managers) { if (managers) {
if (managers.rbmId) participantIds.add(managers.rbmId); if (managers.rbmId) participantIds.add(managers.rbmId);
if (managers.zbhId) participantIds.add(managers.zbhId); if (managers.zbhId) participantIds.add(managers.zbhId);
} }
// 2. National roles // 2. National roles - Crucial for Termination Review
const nationalRoles = [ROLES.DD_LEAD, ROLES.NBH, ROLES.CCO, ROLES.CEO, ROLES.LEGAL_ADMIN]; const nationalRoles = [ROLES.DD_LEAD, ROLES.NBH, ROLES.CCO, ROLES.CEO, ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN];
const nationalUsers = await User.findAll({ const nationalUsers = await User.findAll({
where: { where: {
roleCode: { [Op.in]: nationalRoles }, roleCode: { [Op.in]: nationalRoles },
@ -104,11 +113,13 @@ export class ParticipantService {
nationalUsers.forEach((u: any) => participantIds.add(u.id)); nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 3. Add all unique participants // 3. Add all unique participants
let addedCount = 0;
for (const userId of participantIds) { for (const userId of participantIds) {
await this.addParticipant(termination.id, REQUEST_TYPES.TERMINATION, userId); await this.addParticipant(termination.id, REQUEST_TYPES.TERMINATION, userId);
addedCount++;
} }
console.log(`[ParticipantService] Added ${participantIds.size} participants to termination ${terminationId}`); console.log(`[ParticipantService] Added ${addedCount} participants to termination ${terminationId}`);
} catch (error) { } catch (error) {
console.error('Error assigning termination participants:', error); console.error('Error assigning termination participants:', error);
} }
@ -122,26 +133,26 @@ export class ParticipantService {
const request = await ConstitutionalChange.findByPk(requestId); const request = await ConstitutionalChange.findByPk(requestId);
if (!request) return; if (!request) return;
// In ConstitutionalChange model, dealerId is the User ID
const user = await User.findByPk(request.dealerId);
if (!user || !user.dealerId) {
console.error(`[ParticipantService] No Dealer ID found for user ${request.dealerId}`);
return;
}
const managers = await this.getDealerLocationManagers(user.dealerId);
const participantIds = new Set<string>(); const participantIds = new Set<string>();
// 1. Location based managers // 0. The Dealer (Requester) should be a participant
if (managers) { if (request.dealerId) participantIds.add(request.dealerId);
if (managers.asmId) participantIds.add(managers.asmId);
if (managers.zmId) participantIds.add(managers.zmId); // In ConstitutionalChange model, dealerId is the User ID
if (managers.rbmId) participantIds.add(managers.rbmId); const user = await User.findByPk(request.dealerId);
if (managers.zbhId) participantIds.add(managers.zbhId); if (user && user.dealerId) {
const managers = await this.getDealerLocationManagers(user.dealerId);
// 1. Location based managers
if (managers) {
if (managers.asmId) participantIds.add(managers.asmId);
if (managers.zmId) participantIds.add(managers.zmId);
if (managers.rbmId) participantIds.add(managers.rbmId);
if (managers.zbhId) participantIds.add(managers.zbhId);
}
} }
// 2. National roles // 2. National roles
const nationalRoles = [ROLES.DD_LEAD, ROLES.DD_HEAD, ROLES.NBH, ROLES.LEGAL_ADMIN]; const nationalRoles = [ROLES.DD_LEAD, ROLES.DD_HEAD, ROLES.NBH, ROLES.LEGAL_ADMIN, ROLES.SUPER_ADMIN];
const nationalUsers = await User.findAll({ const nationalUsers = await User.findAll({
where: { where: {
roleCode: { [Op.in]: nationalRoles }, roleCode: { [Op.in]: nationalRoles },
@ -153,11 +164,13 @@ export class ParticipantService {
nationalUsers.forEach((u: any) => participantIds.add(u.id)); nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 3. Add all unique participants // 3. Add all unique participants
let addedCount = 0;
for (const userId of participantIds) { for (const userId of participantIds) {
await this.addParticipant(request.id, REQUEST_TYPES.CONSTITUTIONAL, userId); await this.addParticipant(request.id, REQUEST_TYPES.CONSTITUTIONAL, userId);
addedCount++;
} }
console.log(`[ParticipantService] Added ${participantIds.size} participants to constitutional change ${requestId}`); console.log(`[ParticipantService] Added ${addedCount} participants to constitutional change ${requestId}`);
} catch (error) { } catch (error) {
console.error('Error assigning constitutional participants:', error); console.error('Error assigning constitutional participants:', error);
} }
@ -169,27 +182,43 @@ export class ParticipantService {
static async assignResignationParticipants(requestId: string) { static async assignResignationParticipants(requestId: string) {
try { try {
const resignation = await db.Resignation.findByPk(requestId); const resignation = await db.Resignation.findByPk(requestId);
if (!resignation) return; if (!resignation) {
console.error(`[ParticipantService] Resignation not found: ${requestId}`);
// In Resignation model, dealerId is the User ID
const user = await User.findByPk(resignation.dealerId);
if (!user || !user.dealerId) {
console.error(`[ParticipantService] No Dealer ID found for user ${resignation.dealerId}`);
return; return;
} }
const managers = await this.getDealerLocationManagers(user.dealerId);
const participantIds = new Set<string>(); const participantIds = new Set<string>();
// 1. Location based managers // 0. The Dealer themselves (Requester) should be a participant
if (managers) { if (resignation.dealerId) {
if (managers.asmId) participantIds.add(managers.asmId); participantIds.add(resignation.dealerId);
if (managers.rbmId) participantIds.add(managers.rbmId);
if (managers.zbhId) participantIds.add(managers.zbhId);
} }
// 2. National roles // In Resignation model, dealerId is the User ID
const nationalRoles = [ROLES.DD_LEAD, ROLES.NBH, ROLES.DD_ADMIN, ROLES.FINANCE, ROLES.LEGAL_ADMIN]; const user = await User.findByPk(resignation.dealerId);
// 1. Try to get Location based managers if dealer profile exists
if (user && user.dealerId) {
const managers = await this.getDealerLocationManagers(user.dealerId);
if (managers) {
if (managers.asmId) participantIds.add(managers.asmId);
if (managers.rbmId) participantIds.add(managers.rbmId);
if (managers.zbhId) participantIds.add(managers.zbhId);
}
} else {
console.warn(`[ParticipantService] No Dealer Profile link found for user ${resignation.dealerId}. Only adding national roles.`);
}
// 2. National roles - Essential for workflow transparency
const nationalRoles = [
ROLES.DD_LEAD,
ROLES.NBH,
ROLES.DD_ADMIN,
ROLES.FINANCE,
ROLES.LEGAL_ADMIN,
ROLES.SUPER_ADMIN // Added Super Admin as observer
];
const nationalUsers = await User.findAll({ const nationalUsers = await User.findAll({
where: { where: {
roleCode: { [Op.in]: nationalRoles }, roleCode: { [Op.in]: nationalRoles },
@ -201,11 +230,15 @@ export class ParticipantService {
nationalUsers.forEach((u: any) => participantIds.add(u.id)); nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 3. Add all unique participants // 3. Add all unique participants
let addedCount = 0;
for (const userId of participantIds) { for (const userId of participantIds) {
await this.addParticipant(resignation.id, REQUEST_TYPES.RESIGNATION, userId); // Dealer gets 'owner' type, others get 'contributor'
const pType = userId === resignation.dealerId ? 'owner' : 'contributor';
await this.addParticipant(resignation.id, REQUEST_TYPES.RESIGNATION, userId, pType);
addedCount++;
} }
console.log(`[ParticipantService] Added ${participantIds.size} participants to resignation ${requestId}`); console.log(`[ParticipantService] Added ${addedCount} participants to resignation ${requestId}`);
} catch (error) { } catch (error) {
console.error('Error assigning resignation participants:', error); console.error('Error assigning resignation participants:', error);
} }

30
sync_participants.ts Normal file
View File

@ -0,0 +1,30 @@
import db from './src/database/models/index.js';
import { ParticipantService } from './src/services/ParticipantService';
async function syncAll() {
try {
const terminations = await db.TerminationRequest.findAll();
console.log(`Found ${terminations.length} terminations to sync...`);
for (const term of terminations) {
console.log(`Mapping participants for ${term.requestId} (${term.id})...`);
await ParticipantService.assignTerminationParticipants(term.id);
}
const changes = await db.ConstitutionalChange.findAll();
console.log(`Found ${changes.length} constitutional changes to sync...`);
for (const change of changes) {
console.log(`Mapping participants for ${change.requestId} (${change.id})...`);
await ParticipantService.assignConstitutionalParticipants(change.id);
}
console.log('Sync completed.');
process.exit(0);
} catch (error) {
console.error('Sync failed:', error);
process.exit(1);
}
}
syncAll();

View File

@ -31,7 +31,12 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
} }
async function login(email) { async function login(email) {
const data = await apiRequest('/auth/login', 'POST', { email, password: (email === 'dealer@royalenfield.com' ? 'Admin@123' : PASSWORD) }); const isInternal = email.endsWith('@royalenfield.com') ||
email === 'lince@gmail.com' ||
email === 'yashwin@gmail.com';
const password = isInternal ? 'Admin@123' : 'Dealer@123';
const data = await apiRequest('/auth/login', 'POST', { email, password });
return data.token; return data.token;
} }

View File

@ -94,7 +94,7 @@ async function prospectLogin(phone) {
async function mockUploadDocument(appId, token, docType) { async function mockUploadDocument(appId, token, docType) {
const formData = new FormData(); const formData = new FormData();
const fileBuffer = fs.readFileSync('C:/Users/BACKPACKERS/Pictures/claim_document_type.PNG'); const fileBuffer = fs.readFileSync('/home/laxman-h/Pictures/Screenshots/Screenshot from 2026-03-26 10-08-00.png');
const blob = new Blob([fileBuffer], { type: 'image/png' }); const blob = new Blob([fileBuffer], { type: 'image/png' });
formData.append('file', blob, 'screenshot.png'); formData.append('file', blob, 'screenshot.png');
formData.append('documentType', docType); formData.append('documentType', docType);
@ -364,17 +364,17 @@ async function triggerWorkflow() {
applicationId: applicationUUID, applicationId: applicationUUID,
amount: 500000, amount: 500000,
paymentReference: 'PAY-888999', paymentReference: 'PAY-888999',
depositType: 'INITIAL', depositType: 'SECURITY_DEPOSIT',
status: 'Verified' status: 'Verified'
}, financeToken); }, financeToken);
log(9, 'Initial Security Deposit Verified.'); log(9, 'Security Deposit Verified.');
log(9.1, 'Finance Verifying FINAL Security Deposit (₹15L)...'); log(9.1, '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,
paymentReference: 'PAY-FIN-999', paymentReference: 'PAY-FIN-999',
depositType: 'FINAL', depositType: 'FIRST_FILL',
status: 'Verified' status: 'Verified'
}, financeToken); }, financeToken);
log(9.1, 'Final Security Deposit Verified.'); log(9.1, 'Final Security Deposit Verified.');
@ -383,14 +383,14 @@ async function triggerWorkflow() {
// 10. FINAL LOA APPROVAL // 10. FINAL LOA APPROVAL
log(10, 'NBH & Head Approving Final LOA...'); log(10, '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);
loaRequestId = loaRes.data.id; const finalLoaRequestId = loaRes.data.id;
await apiRequest(`/loa/request/${loaRequestId}/approve`, 'POST', { await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
action: 'Approved', action: 'Approved',
remarks: 'Head Authorization (Level 1)' remarks: 'Head Authorization (Level 1)'
}, headToken); }, headToken);
await apiRequest(`/loa/request/${loaRequestId}/approve`, 'POST', { await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
action: 'Approved', action: 'Approved',
remarks: 'NBH Approval (Level 2)' remarks: 'NBH Approval (Level 2)'
}, nbhToken); }, nbhToken);