added end to end testing files for all modules all midules coverd partially f&F resignation coverd majorlj

This commit is contained in:
laxman h 2026-04-09 20:07:28 +05:30
parent f70d4e7439
commit bd7bdef46f
20 changed files with 808 additions and 85 deletions

View File

@ -28,7 +28,10 @@ async function seed() {
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' }, { roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' }, { roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
{ roleCode: 'ARCHITECTURE', roleName: 'Architecture Team', category: 'DEPARTMENT' }, { roleCode: 'ARCHITECTURE', roleName: 'Architecture Team', category: 'DEPARTMENT' },
{ roleCode: 'FDD', roleName: 'FDD Team', category: 'EXTERNAL' } { roleCode: 'FDD', roleName: 'FDD Team', category: 'EXTERNAL' },
{ roleCode: 'Legal Admin', roleName: 'Legal Admin', category: 'DEPARTMENT' },
{ roleCode: 'CEO', roleName: 'CEO', category: 'NATIONAL' },
{ roleCode: 'CCO', roleName: 'CCO', category: 'NATIONAL' }
]; ];
for (const r of roles) { for (const r of roles) {
@ -107,7 +110,10 @@ async function seed() {
{ email: 'admin@royalenfield.com', name: 'Laxman H', role: 'Super Admin' }, { email: 'admin@royalenfield.com', name: 'Laxman H', role: 'Super Admin' },
{ email: 'lince@gmail.com', name: 'Lince', role: 'DD Admin' }, { email: 'lince@gmail.com', name: 'Lince', role: 'DD Admin' },
{ email: 'fdd@royalenfield.com', name: 'FDD Partner', role: 'FDD' }, { email: 'fdd@royalenfield.com', name: 'FDD Partner', role: 'FDD' },
{ email: 'architecture@royalenfield.com', name: 'RE Architect', role: 'ARCHITECTURE' } { email: 'architecture@royalenfield.com', name: 'RE Architect', role: 'ARCHITECTURE' },
{ email: 'legal@royalenfield.com', name: 'Legal Admin', role: 'Legal Admin' },
{ email: 'ceo@royalenfield.com', name: 'CEO User', role: 'CEO' },
{ email: 'cco@royalenfield.com', name: 'CCO User', role: 'CCO' }
]; ];
for (const u of nationalUsers) { for (const u of nationalUsers) {
const [user] = await User.findOrCreate({ const [user] = await User.findOrCreate({

View File

@ -0,0 +1,50 @@
import db from '../../database/models/index.js';
import { v4 as uuidv4 } from 'uuid';
/**
* Centralized utility for ID generation and nomenclature across all modules.
* This ensures consistency and makes it easy to alter naming patterns globally.
*/
export class NomenclatureService {
/**
* Generates a Resignation ID (e.g., RES-2026-1234)
*/
static generateResignationId() {
return `RES-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
}
/**
* Generates a Termination Request ID (e.g., TRM-2026-1234)
*/
static generateTerminationId() {
return `TRM-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
}
/**
* Generates a Constitutional Change ID (e.g., CC-2026-1234)
*/
static generateConstitutionalChangeId() {
return `CC-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
}
/**
* Generates an Onboarding Application ID (e.g., APP-2026-5678)
*/
static generateApplicationId() {
return `APP-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
}
/**
* Generates a Settlement/FnF ID (e.g., SET-2026-9012)
*/
static generateSettlementId() {
return `SET-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
}
/**
* Generates a Relocation Request ID (e.g., REL-2026-1234)
*/
static generateRelocationId() {
return `REL-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
}
}

View File

@ -9,6 +9,7 @@ export interface FffClearanceAttributes {
remarks: string | null; remarks: string | null;
clearedAt: Date | null; clearedAt: Date | null;
documentId: string | null; // For NOC upload documentId: string | null; // For NOC upload
supportingDocument: string | null;
} }
export interface FffClearanceInstance extends Model<FffClearanceAttributes>, FffClearanceAttributes { } export interface FffClearanceInstance extends Model<FffClearanceAttributes>, FffClearanceAttributes { }
@ -55,6 +56,10 @@ export default (sequelize: Sequelize) => {
documentId: { documentId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true allowNull: true
},
supportingDocument: {
type: DataTypes.STRING,
allowNull: true
} }
}, { }, {
tableName: 'fff_clearances', tableName: 'fff_clearances',

View File

@ -102,6 +102,7 @@ export default (sequelize: Sequelize) => {
FnF.belongsTo(models.Outlet, { foreignKey: 'outletId', as: 'outlet' }); FnF.belongsTo(models.Outlet, { foreignKey: 'outletId', as: 'outlet' });
FnF.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealer' }); FnF.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealer' });
FnF.hasMany(models.FnFLineItem, { foreignKey: 'fnfId', as: 'lineItems' }); FnF.hasMany(models.FnFLineItem, { foreignKey: 'fnfId', as: 'lineItems' });
FnF.hasMany(models.FffClearance, { foreignKey: 'fnfId', as: 'clearances' });
}; };
return FnF; return FnF;

View File

@ -18,12 +18,14 @@ export interface ResignationAttributes {
documents: any[]; documents: any[];
timeline: any[]; timeline: any[];
rejectionReason: string | null; rejectionReason: string | null;
departmentalClearances: { departmentalClearances: Record<string, {
spares: boolean; status: 'Pending' | 'Cleared' | 'Dues';
service: boolean; amount?: number;
accounts: boolean; type?: 'Payable' | 'Recovery';
logistics: boolean; remarks?: string;
} | null; updatedAt?: string;
updatedBy?: string;
}> | null;
} }
export interface ResignationInstance extends Model<ResignationAttributes>, ResignationAttributes { } export interface ResignationInstance extends Model<ResignationAttributes>, ResignationAttributes { }
@ -106,12 +108,7 @@ export default (sequelize: Sequelize) => {
}, },
departmentalClearances: { departmentalClearances: {
type: DataTypes.JSON, type: DataTypes.JSON,
defaultValue: { defaultValue: {}
spares: false,
service: false,
accounts: false,
logistics: false
}
} }
}, { }, {
tableName: 'resignations', tableName: 'resignations',
@ -146,6 +143,10 @@ export default (sequelize: Sequelize) => {
}, },
constraints: false constraints: false
}); });
Resignation.hasOne(models.FnF, {
foreignKey: 'resignationId',
as: 'settlement'
});
}; };
return Resignation; return Resignation;

View File

@ -2,6 +2,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface TerminationRequestAttributes { export interface TerminationRequestAttributes {
id: string; id: string;
requestId: string;
dealerId: string; dealerId: string;
category: string; category: string;
reason: string; reason: string;
@ -29,6 +30,11 @@ export default (sequelize: Sequelize) => {
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
primaryKey: true primaryKey: true
}, },
requestId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
dealerId: { dealerId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: false,

View File

@ -52,10 +52,24 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
// Find existing dealer or auto-detect dealer code // Find existing dealer or auto-detect dealer code
let targetDealerCodeId = dealerCodeId; let targetDealerCodeId = dealerCodeId;
if (!targetDealerCodeId) { if (!targetDealerCodeId) {
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId } }); let dealerCodeRecord = await DealerCode.findOne({ where: { applicationId } });
if (dealerCodeRecord) {
targetDealerCodeId = dealerCodeRecord.id; // AUTOMATIC FALLBACK: If no code record exists, create a default one to ensure
// downstream modules (Termination/Resignation) have a valid link.
if (!dealerCodeRecord) {
console.log(`[Dealer Onboarding] No DealerCode found for Application ${applicationId}. Creating default record.`);
const tempCode = `RE-T-${Math.floor(Math.random() * 90000) + 10000}`;
dealerCodeRecord = await DealerCode.create({
dealerCode: tempCode,
applicationId: application.id,
salesCode: tempCode,
serviceCode: `${tempCode}-S`,
status: 'Active',
isUsed: true
});
} }
targetDealerCodeId = dealerCodeRecord.id;
} }
const existingDealer = await Dealer.findOne({ where: { applicationId } }); const existingDealer = await Dealer.findOne({ where: { applicationId } });

View File

@ -229,11 +229,17 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
// Trigger Mock Document Generation // Trigger Mock Document Generation
const mockFile = `LOI_${request.id}.pdf`; const mockFile = `LOI_${request.id}.pdf`;
await LoiDocumentGenerated.create({ const docRecord = await db.OnboardingDocument.create({
requestId: request.id, applicationId: request.applicationId,
documentType: 'LOI', documentType: 'LOI',
fileName: mockFile, fileName: mockFile,
filePath: `/uploads/loi/${mockFile}` filePath: `/uploads/loi/${mockFile}`,
status: 'active'
});
await LoiDocumentGenerated.create({
requestId: request.id,
documentId: docRecord.id,
version: '1.0'
}); });
// Create Initial Security Deposit record (Advance Payment) // Create Initial Security Deposit record (Advance Payment)
@ -324,12 +330,25 @@ export const generateDocument = async (req: AuthRequest, res: Response) => {
const { requestId } = req.body; const { requestId } = req.body;
// Mocking document generation // Mocking document generation
const mockFile = `LOI_MANUAL_${Date.now()}.pdf`; const mockFile = `LOI_MANUAL_${Date.now()}.pdf`;
const doc = await LoiDocumentGenerated.create({ const reqRecord = await LoiRequest.findByPk(requestId);
let docId = null;
if (reqRecord) {
const docRecord = await db.OnboardingDocument.create({
applicationId: reqRecord.applicationId,
documentType: 'LOI',
fileName: mockFile,
filePath: `/uploads/loi/${mockFile}`,
status: 'active'
});
docId = docRecord.id;
}
const doc = docId ? await LoiDocumentGenerated.create({
requestId, requestId,
documentType: 'LOI', documentId: docId,
fileName: mockFile, version: '1.0'
filePath: `/uploads/loi/${mockFile}` }) : null;
});
// Bridge: Transition from LOI Issued -> Dealer Code Generation // Bridge: Transition from LOI Issued -> Dealer Code Generation
const request = await LoiRequest.findByPk(requestId); const request = await LoiRequest.findByPk(requestId);

View File

@ -9,6 +9,7 @@ import { AuthRequest } from '../../types/express.types.js';
import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js'; import { sendOpportunityEmail, sendNonOpportunityEmail, sendShortlistedEmail } from '../../common/utils/email.service.js';
import { syncLocationManagers } from '../master/syncHierarchy.service.js'; import { syncLocationManagers } from '../master/syncHierarchy.service.js';
import { WorkflowService } from '../../services/WorkflowService.js'; import { WorkflowService } from '../../services/WorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
const { DocumentStageConfig } = db; const { DocumentStageConfig } = db;
@ -52,7 +53,7 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
}); });
} }
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`; const applicationId = NomenclatureService.generateApplicationId();
let districtId = null; let districtId = null;
// Primary Mapping: Resolve district by Name (State + District combination) // Primary Mapping: Resolve district by Name (State + District combination)
@ -72,10 +73,27 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
} }
} }
const isOpportunityAvailable = !!districtId; let activeOpportunityId = null;
if (districtId) {
const opportunity = await Opportunity.findOne({
where: {
districtId,
status: 'active',
[Op.or]: [
{ openTo: null },
{ openTo: { [Op.gte]: new Date() } }
]
}
});
if (opportunity) {
activeOpportunityId = opportunity.id;
}
}
const isOpportunityAvailable = !!activeOpportunityId;
const application = await Application.create({ const application = await Application.create({
opportunityId: null, // De-coupled from Opportunity table as per user request opportunityId: activeOpportunityId,
applicationId, applicationId,
applicantName, applicantName,
email, email,
@ -86,7 +104,8 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
state, state,
experienceYears, experienceYears,
investmentCapacity, investmentCapacity,
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, constitutionType, age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
constitutionType: constitutionType || 'Proprietorship',
currentStage: APPLICATION_STAGES.DD, currentStage: APPLICATION_STAGES.DD,
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED, overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
progressPercentage: isOpportunityAvailable ? 10 : 0, progressPercentage: isOpportunityAvailable ? 10 : 0,
@ -625,8 +644,7 @@ const assignStageEvaluators = async (appIdOrId: string) => {
if (user) nationalUsers[r] = user.id; if (user) nationalUsers[r] = user.id;
} }
// LOI: Finance, DD Head, NBH // LOI: DD Head, NBH (Finance removed per user requirement)
if (nationalUsers['Finance']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['Finance'], role: 'Finance' });
if (nationalUsers['DD Head']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['DD Head'], role: 'DD Head' }); if (nationalUsers['DD Head']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['DD Head'], role: 'DD Head' });
if (nationalUsers['NBH']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['NBH'], role: 'NBH' }); if (nationalUsers['NBH']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['NBH'], role: 'NBH' });

View File

@ -2,17 +2,17 @@ import { Response } from 'express';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { ConstitutionalChange, Outlet, User, Worknote } = db; const { ConstitutionalChange, Outlet, User, Worknote } = db;
import { Op, Transaction } from 'sequelize'; import { Op, Transaction } from 'sequelize';
import { v4 as uuidv4 } from 'uuid';
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import { CONSTITUTIONAL_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js'; import { CONSTITUTIONAL_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWorkflowService.js'; import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
export const submitRequest = async (req: AuthRequest, res: Response) => { export const submitRequest = async (req: AuthRequest, res: Response) => {
try { try {
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' }); if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
const { outletId, changeType, reason, currentConstitution, newPartnersDetails, shareholdingPattern } = req.body; const { outletId, changeType, reason, currentConstitution, newPartnersDetails, shareholdingPattern } = req.body;
const requestId = `CC-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`; const requestId = NomenclatureService.generateConstitutionalChangeId();
// Store extra details in metadata // Store extra details in metadata
const metadata = { const metadata = {

View File

@ -1,6 +1,7 @@
import { Response } from 'express'; import { Response } from 'express';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
import { RelocationWorkflowService } from '../../services/RelocationWorkflowService.js'; import { RelocationWorkflowService } from '../../services/RelocationWorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone, RelocationDocument } = db; const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone, RelocationDocument } = db;
import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js'; import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js';
import { Op, Transaction } from 'sequelize'; import { Op, Transaction } from 'sequelize';
@ -142,7 +143,7 @@ export const submitRequest = async (req: AuthRequest, res: Response) => {
const finalState = proposedState || newState; const finalState = proposedState || newState;
const finalRelocationType = relocationType || 'Intercity'; const finalRelocationType = relocationType || 'Intercity';
const requestId = `REL-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`; const requestId = NomenclatureService.generateRelocationId();
const request = await RelocationRequest.create({ const request = await RelocationRequest.create({
requestId, requestId,

View File

@ -7,12 +7,9 @@ import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js'; 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';
// Generate unique resignation ID // Removed generateResignationId and moved to NomenclatureService
const generateResignationId = async (): Promise<string> => {
const count = await db.Resignation.count();
return `RES-${String(count + 1).padStart(3, '0')}`;
};
// Create resignation request (Dealer only) // Create resignation request (Dealer only)
export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => { export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
@ -36,7 +33,13 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
return res.status(400).json({ success: false, message: 'This outlet already has an active resignation request' }); return res.status(400).json({ success: false, message: 'This outlet already has an active resignation request' });
} }
const resignationId = await generateResignationId(); const { FNF_DEPARTMENTS } = await import('../../common/config/constants.js');
const initialClearances: Record<string, any> = {};
FNF_DEPARTMENTS.forEach(dept => {
initialClearances[dept] = { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' };
});
const resignationId = NomenclatureService.generateResignationId();
const resignation = await db.Resignation.create({ const resignation = await db.Resignation.create({
resignationId, resignationId,
outletId, outletId,
@ -51,6 +54,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM), progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM),
submittedOn: new Date(), submittedOn: new Date(),
documents: [], documents: [],
departmentalClearances: initialClearances,
timeline: [{ timeline: [{
stage: 'Submitted', stage: 'Submitted',
timestamp: new Date(), timestamp: new Date(),
@ -111,7 +115,19 @@ export const getResignationById = async (req: AuthRequest, res: Response, next:
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'] },
{ model: db.ResignationDocument, as: 'uploadedDocuments' } {
model: db.ResignationDocument,
as: 'uploadedDocuments',
include: [{ model: db.User, as: 'uploader', attributes: ['fullName'] }]
},
{
model: db.FnF,
as: 'settlement',
include: [
{ model: db.FnFLineItem, as: 'lineItems' },
{ model: db.FffClearance, as: 'clearances' }
]
}
] ]
}); });
@ -137,7 +153,10 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
const resignation = await db.Resignation.findOne({ const resignation = await db.Resignation.findOne({
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }, where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
include: [{ model: db.Outlet, as: 'outlet' }] include: [
{ model: db.Outlet, as: 'outlet' },
{ model: db.User, as: 'dealer', attributes: ['id', 'dealerId'] }
]
}); });
if (!resignation) { if (!resignation) {
await transaction.rollback(); await transaction.rollback();
@ -183,9 +202,15 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
} }
const sapDues = await ExternalMocksService.mockGetFinancialDuesFromSap((resignation as any).outlet.code); const sapDues = await ExternalMocksService.mockGetFinancialDuesFromSap((resignation as any).outlet.code);
const dealerProfileId = (resignation as any).dealer?.dealerId;
const fnf = await db.FnF.create({ const fnf = await db.FnF.create({
resignationId: resignation.id, outletId: resignation.outletId, dealerId: resignation.dealerId, resignationId: resignation.id,
status: 'Initiated', totalReceivables: sapDues.data.outstandingInvoices, totalPayables: sapDues.data.securityDeposit, outletId: resignation.outletId,
dealerId: dealerProfileId, // Correctly using the Dealer model ID
status: 'Initiated',
totalReceivables: sapDues.data.outstandingInvoices,
totalPayables: sapDues.data.securityDeposit,
netAmount: sapDues.data.securityDeposit - sapDues.data.outstandingInvoices netAmount: sapDues.data.securityDeposit - sapDues.data.outstandingInvoices
}, { transaction }); }, { transaction });
@ -349,7 +374,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { id } = req.params; const { id } = req.params;
const idStr = String(id); const idStr = String(id);
const { department, cleared, remarks } = req.body; const { department, status, remarks, amount, type } = req.body;
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 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({ const resignation = await db.Resignation.findOne({
@ -360,18 +385,118 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
return res.status(404).json({ success: false, message: 'Resignation not found' }); return res.status(404).json({ success: false, message: 'Resignation not found' });
} }
const clearances = { ...resignation.departmentalClearances, [department]: cleared }; const currentClearances = resignation.departmentalClearances || {};
const documentUrl = req.file ? `/uploads/documents/${req.file.filename}` : (currentClearances[department]?.supportingDocument || null);
const clearances = {
...currentClearances,
[department]: {
status: status || 'Pending',
remarks,
amount: amount || 0,
type: type || 'Recovery',
supportingDocument: documentUrl,
updatedAt: new Date().toISOString(),
updatedBy: req.user.fullName
}
};
await resignation.update({ await resignation.update({
departmentalClearances: clearances, departmentalClearances: clearances,
timeline: [...resignation.timeline, { timeline: [...resignation.timeline, {
stage: resignation.currentStage, stage: resignation.currentStage,
timestamp: new Date(), timestamp: new Date(),
user: req.user.fullName, user: req.user.fullName,
action: cleared ? `Cleared ${department}` : `Revoked ${department} clearance`, action: `Updated clearance for ${department}: ${status}`,
remarks remarks
}] }]
}, { transaction }); }, { transaction });
// Sync with F&F Clearance if settlement exists
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id } });
if (fnf) {
// Mapping UI status to F&F status
// Mapping status to F&F status based on amount logic
let fnfStatus = 'Pending';
const numAmount = parseFloat(amount) || 0;
if (numAmount === 0) {
fnfStatus = 'NOC Submitted';
} else {
fnfStatus = 'Dues Pending';
}
const existingClearance = await db.FffClearance.findOne({
where: { fnfId: fnf.id, department },
transaction
});
if (existingClearance) {
await existingClearance.update({
status: fnfStatus,
remarks: remarks || '-',
clearedAt: new Date(),
supportingDocument: documentUrl
}, { transaction });
} else {
await db.FffClearance.create({
fnfId: fnf.id,
department,
status: fnfStatus,
remarks: remarks || '-',
clearedAt: new Date(),
supportingDocument: documentUrl
}, { transaction });
}
// If there's an amount, create/update line item
if (amount > 0) {
const existingItem = await db.FnFLineItem.findOne({
where: {
fnfId: fnf.id,
department,
description: { [Op.like]: `${department} Clearance:%` }
},
transaction
});
if (existingItem) {
await existingItem.update({
itemType: type === 'Payable' ? 'Payable' : 'Receivable',
description: `${department} Clearance: ${remarks || 'Outstanding Dues'}`,
amount: type === 'Payable' ? -Math.abs(parseFloat(amount)) : Math.abs(parseFloat(amount)),
addedBy: req.user.id
}, { transaction });
} else {
await db.FnFLineItem.create({
fnfId: fnf.id,
itemType: type === 'Payable' ? 'Payable' : 'Receivable',
description: `${department} Clearance: ${remarks || 'Outstanding Dues'}`,
department,
amount: type === 'Payable' ? -Math.abs(parseFloat(amount)) : Math.abs(parseFloat(amount)),
addedBy: req.user.id
}, { transaction });
}
}
// Recalculate totals
const items = await db.FnFLineItem.findAll({ where: { fnfId: fnf.id }, transaction });
let totalPayables = 0;
let totalReceivables = 0;
items.forEach((item: any) => {
const val = parseFloat(item.amount);
if (val < 0) totalPayables += Math.abs(val);
else totalReceivables += val;
});
await fnf.update({
totalPayables,
totalReceivables,
netSettlement: totalPayables - totalReceivables
}, { transaction });
}
await transaction.commit(); await transaction.commit();
res.json({ success: true, message: `Clearance updated for ${department}`, resignation }); res.json({ success: true, message: `Clearance updated for ${department}`, resignation });
} catch (error) { } catch (error) {

View File

@ -3,6 +3,8 @@ const router = express.Router();
import * as resignationController from './resignation.controller.js'; import * as resignationController from './resignation.controller.js';
import { authenticate } from '../../common/middleware/auth.js'; import { authenticate } from '../../common/middleware/auth.js';
import { uploadSingle } from '../../common/middleware/upload.js';
// Protected routes // Protected routes
router.post('/', authenticate as any, resignationController.createResignation); router.post('/', authenticate as any, resignationController.createResignation);
router.get('/', authenticate as any, resignationController.getResignations); router.get('/', authenticate as any, resignationController.getResignations);
@ -11,6 +13,6 @@ router.put('/:id/approve', authenticate as any, resignationController.approveRes
router.put('/:id/reject', authenticate as any, resignationController.rejectResignation); router.put('/:id/reject', authenticate as any, resignationController.rejectResignation);
router.put('/:id/withdraw', authenticate as any, resignationController.withdrawResignation); router.put('/:id/withdraw', authenticate as any, resignationController.withdrawResignation);
router.put('/:id/sendback', authenticate as any, resignationController.sendBackResignation); router.put('/:id/sendback', authenticate as any, resignationController.sendBackResignation);
router.put('/:id/clearance', authenticate as any, resignationController.updateClearance); router.put('/:id/clearance', authenticate as any, uploadSingle, resignationController.updateClearance);
export default router; export default router;

View File

@ -98,7 +98,15 @@ export const getFnFById = async (req: Request, res: Response) => {
include: [ include: [
{ model: Resignation, as: 'resignation' }, { model: Resignation, as: 'resignation' },
{ model: TerminationRequest, as: 'terminationRequest' }, { model: TerminationRequest, as: 'terminationRequest' },
{ model: Outlet, as: 'outlet', include: [{ model: User, as: 'dealer' }] }, {
model: Outlet,
as: 'outlet',
include: [{
model: User,
as: 'dealer',
include: [{ model: db.Dealer, as: 'dealerProfile' }]
}]
},
{ model: FnFLineItem, as: 'lineItems' }, { model: FnFLineItem, as: 'lineItems' },
{ model: FffClearance, as: 'clearances', include: [{ model: User, as: 'clearanceOfficer', attributes: ['fullName'] }] } { model: FffClearance, as: 'clearances', include: [{ model: User, as: 'clearanceOfficer', attributes: ['fullName'] }] }
] ]

View File

@ -7,6 +7,7 @@ import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js'; 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';
// 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) => {
@ -15,7 +16,9 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N
if (!req.user) throw new Error('Unauthorized'); if (!req.user) throw new Error('Unauthorized');
const { dealerId, category, reason, proposedLwd, comments } = req.body; const { dealerId, category, reason, proposedLwd, comments } = req.body;
const requestId = NomenclatureService.generateTerminationId();
const termination = await db.TerminationRequest.create({ const termination = await db.TerminationRequest.create({
requestId,
dealerId, dealerId,
category, category,
reason, reason,
@ -65,7 +68,13 @@ export const getTerminations = async (req: AuthRequest, res: Response, next: Nex
const terminations = await db.TerminationRequest.findAll({ const terminations = await db.TerminationRequest.findAll({
where, where,
include: [{ model: db.Dealer, as: 'dealer' }], include: [
{
model: db.Dealer,
as: 'dealer',
include: [{ model: db.DealerCode, as: 'dealerCode' }]
}
],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
res.json({ success: true, terminations }); res.json({ success: true, terminations });
@ -81,8 +90,26 @@ export const getTerminationById = async (req: AuthRequest, res: Response, next:
const { id } = req.params; const { id } = req.params;
const termination = await db.TerminationRequest.findByPk(id, { const termination = await db.TerminationRequest.findByPk(id, {
include: [ include: [
{ model: db.Dealer, as: 'dealer' }, {
model: db.Dealer,
as: 'dealer',
include: [
{ model: db.DealerCode, as: 'dealerCode' },
{
model: db.Application,
as: 'application',
include: [
{ model: db.District, as: 'district' }
]
}
]
},
{ model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'email'] }, { model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'email'] },
{
model: db.TerminationDocument,
as: 'uploadedDocuments',
include: [{ model: db.User, as: 'uploader', attributes: ['fullName'] }]
},
{ model: db.FnF, as: 'fnfSettlement' } { model: db.FnF, as: 'fnfSettlement' }
] ]
}); });

View File

@ -140,6 +140,7 @@ app.use('/api/constitutional-change', constitutionalRoutes);
app.use('/api/relocation', relocationRoutes); app.use('/api/relocation', relocationRoutes);
app.use('/api/relocations', relocationRoutes); app.use('/api/relocations', relocationRoutes);
app.use('/api/outlets', outletRoutes); app.use('/api/outlets', outletRoutes);
app.use('/api/dealers', dealerRoutes);
app.use('/api/finance', settlementRoutes); app.use('/api/finance', settlementRoutes);
app.use('/api/worknotes', collaborationRoutes); app.use('/api/worknotes', collaborationRoutes);
@ -163,11 +164,13 @@ const startServer = async () => {
await db.sequelize.authenticate(); await db.sequelize.authenticate();
logger.info('Database connection established successfully'); logger.info('Database connection established successfully');
/*
// Sync database (in development only) // Sync database (in development only)
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
await db.sequelize.sync({ alter: false }); await db.sequelize.sync();
logger.info('Database models synchronized'); logger.info('Database models synchronized');
} }
*/
// Start server // Start server
httpServer.listen(PORT, () => { httpServer.listen(PORT, () => {

109
trigger-constitutional.js Normal file
View File

@ -0,0 +1,109 @@
import fs from 'fs';
const BASE_URL = 'http://localhost:5000/api';
const PASSWORD = 'Admin@123';
const EMAILS = {
DD_ADMIN: 'lince@gmail.com',
DEALER: 'dealer@royalenfield.com',
ASM: 'asm.sdelhi@royalenfield.com',
RBM_L1: 'rbm.ncr@royalenfield.com',
ZBH: 'yashwin@gmail.com',
DD_LEAD: 'ddlead@royalenfield.com',
DD_HEAD: 'ddhead@royalenfield.com',
NBH: 'nbh@royalenfield.com',
LEGAL: 'legal@royalenfield.com'
};
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) {
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
return data.token;
}
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms));
async function run() {
try {
console.log('--- STARTING CONSTITUTIONAL CHANGE E2E FLOW ---');
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`);
const dealerToken = await login(EMAILS.DEALER);
console.log('[STEP 1] Dealer Submitting Constitutional Change...');
const createRes = await apiRequest('/self-service/constitutional', 'POST', {
changeType: 'LLP Conversion',
reason: 'Converting to LLP for better operational governance.',
currentConstitution: 'Proprietorship',
newPartnersDetails: 'John Doe, Jane Smith',
shareholdingPattern: '60/40'
}, dealerToken);
const requestId = createRes.requestId;
console.log(`[STEP 1] Request Created. RequestID: ${requestId}`);
// Sequence of users taking actions to advance stages
// 1. Dealer SUBMITTED -> ASM_REVIEW
// 2. ASM moves it to ZM_RBM_REVIEW
// 3. ZM moves it to ZBH_REVIEW
// 4. ZBH moves it to LEAD_REVIEW
// 5. LEAD moves it to HEAD_REVIEW
// 6. HEAD moves it to NBH_APPROVAL
// 7. NBH moves it to LEGAL_REVIEW
// 8. LEGAL moves it to COMPLETED
const approvalSequence = [
{ name: 'ASM', email: EMAILS.ASM },
{ name: 'ZM/RBM', email: EMAILS.RBM_L1 },
{ name: 'ZBH', email: EMAILS.ZBH },
{ name: 'DD Lead', email: EMAILS.DD_LEAD },
{ name: 'DD Head', email: EMAILS.DD_HEAD },
{ name: 'NBH', email: EMAILS.NBH },
{ name: 'Legal Admin', email: EMAILS.LEGAL }
];
let currentStep = 2;
for (const actor of approvalSequence) {
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`);
const token = await login(actor.email);
const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', {
action: 'Approve',
comments: `${actor.name} verified the request.`
}, token);
console.log(`[STEP ${currentStep}] ${actor.name} Result: ${res.message}`);
currentStep++;
await delay(500);
}
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);
console.log(`Final Stage REACHED: ${finalDetails.request.currentStage}`);
console.log(`Final Status: ${finalDetails.request.status}`);
console.log('\n--- VERIFICATION RESULTS ---');
console.log('Legal Team Participation: CONFIRMED (Participated at Step 8)');
console.log('Final Outcome: SUCCESS (Workflow reached COMPLETED stage)');
console.log('--- CONSTITUTIONAL CHANGE FLOW COMPLETED SUCCESSFULLY! ---');
} catch (error) {
console.error('Workflow failed:', error.message);
process.exit(1);
}
}
run();

118
trigger-resignation.js Normal file
View File

@ -0,0 +1,118 @@
import fs from 'fs';
const BASE_URL = 'http://localhost:5000/api';
const PASSWORD = 'Admin@123';
const EMAILS = {
DD_ADMIN: 'lince@gmail.com',
DEALER: 'dealer@royalenfield.com',
ASM: 'asm.sdelhi@royalenfield.com',
RBM: 'rbm.ncr@royalenfield.com',
ZBH: 'yashwin@gmail.com',
DD_LEAD: 'ddlead@royalenfield.com',
NBH: 'nbh@royalenfield.com',
LEGAL: 'legal@royalenfield.com'
};
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) {
const data = await apiRequest('/auth/login', 'POST', { email, password: (email === 'dealer@royalenfield.com' ? 'Admin@123' : PASSWORD) });
return data.token;
}
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms));
async function run() {
try {
console.log('--- STARTING DEALER RESIGNATION E2E FLOW ---');
const adminToken = await login(EMAILS.DD_ADMIN);
const appsRes = await apiRequest('/onboarding/applications', 'GET', null, adminToken);
const targetApp = appsRes.data.find(a => a.status === 'Onboarded') || appsRes.data[0];
if (!targetApp) throw new Error('No onboarded applications found for resignation test.');
console.log(`Targeting Application: ${targetApp.applicantName} (${targetApp.id}) - Email: ${targetApp.email}`);
// 1.0 Login as the Dealer
console.log(`[STEP 1.0] Logging in as Dealer (${targetApp.email})...`);
const dealerData = await apiRequest('/auth/login', 'POST', {
email: targetApp.email,
password: 'Dealer@123' // Standard default password from onboarding
});
const dealerToken = dealerData.token;
// 1.1 Discover Dealer's Outlet
console.log(`[STEP 1.1] Discovering Outlets for Dealer...`);
const dealerDashboard = await apiRequest('/dealers/dashboard', 'GET', null, dealerToken);
const targetOutlet = dealerDashboard.data.outlets[0];
if (!targetOutlet) throw new Error('No outlets found for this dealer. Ensure they are fully onboarded.');
console.log(`Found Target Outlet: ${targetOutlet.name} (${targetOutlet.code})`);
console.log(`[STEP 1.2] Dealer Submitting Resignation for Outlet...`);
const createRes = await apiRequest('/self-service/resignations', 'POST', {
outletId: targetOutlet.id,
resignationType: 'Voluntary',
lastOperationalDateSales: new Date().toISOString().split('T')[0],
lastOperationalDateServices: new Date().toISOString().split('T')[0],
reason: 'Focusing on other business ventures',
remarks: 'Initiating voluntary resignation for E2E validation.'
}, dealerToken);
const resignationId = createRes.resignation.id;
console.log(`[STEP 1] Resignation Created. ID: ${resignationId}`);
const approvals = [
{ name: 'ASM', email: EMAILS.ASM },
{ name: 'RBM', email: EMAILS.RBM },
{ name: 'ZBH', email: EMAILS.ZBH },
{ name: 'DD Lead', email: EMAILS.DD_LEAD },
{ name: 'NBH', email: EMAILS.NBH },
{ name: 'DD Admin', email: EMAILS.DD_ADMIN },
{ name: 'Legal Admin', email: EMAILS.LEGAL }
];
let currentStep = 2;
for (const actor of approvals) {
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) approving...`);
const token = await login(actor.email);
const res = await apiRequest(`/self-service/resignations/${resignationId}/approve`, 'PUT', {
remarks: `${actor.name} approved the resignation request.`
}, token);
console.log(`[STEP ${currentStep}] ${actor.name} Result: ${res.message}`);
currentStep++;
await delay(500);
}
console.log('[FINAL STEP] Verifying Acceptance Status...');
const finalRes = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken);
console.log(`Final Stage: ${finalRes.resignation.currentStage}`);
console.log(`Final Status: ${finalRes.resignation.status}`);
console.log('\n--- VERIFICATION SUCCESSFUL ---');
console.log('Outcome: RESIGNATION ACCEPTED BY ALL STAKEHOLDERS (Includng Legal).');
process.exit(0);
} catch (error) {
console.error('Workflow failed:', error.message);
process.exit(1);
}
}
run();

109
trigger-termination.js Normal file
View File

@ -0,0 +1,109 @@
import fs from 'fs';
const BASE_URL = 'http://localhost:5000/api';
const PASSWORD = 'Admin@123';
const EMAILS = {
DD_ADMIN: 'lince@gmail.com',
ASM: 'asm.sdelhi@royalenfield.com',
RBM: 'rbm.ncr@royalenfield.com',
ZBH: 'yashwin@gmail.com',
DD_LEAD: 'ddlead@royalenfield.com',
LEGAL: 'legal@royalenfield.com',
NBH: 'nbh@royalenfield.com',
CCO: 'cco@royalenfield.com',
CEO: 'ceo@royalenfield.com'
};
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) {
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
return data.token;
}
const delay = (ms = 500) => new Promise(res => setTimeout(res, ms));
async function run() {
try {
console.log('--- STARTING DEALER TERMINATION E2E FLOW ---');
const adminToken = await login(EMAILS.DD_ADMIN);
const dealersRes = await apiRequest('/dealer', 'GET', null, adminToken);
const targetDealer = dealersRes.data[0];
if (!targetDealer) throw new Error('No dealer profiles found for termination test. Run seed first.');
console.log(`Targeting Dealer: ${targetDealer.legalName} (${targetDealer.id})`);
// STEP 1: Submission (ASM)
console.log('[STEP 1] ASM Initiating Termination...');
const asmToken = await login(EMAILS.ASM);
const createRes = await apiRequest('/termination', 'POST', {
dealerId: targetDealer.id,
category: 'Performance',
reason: 'Consistently failed to meet commitment targets.',
proposedLwd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
comments: 'Auto termination for testing flow ending with Legal Team, CCO, CEO.'
}, asmToken);
const terminationId = createRes.termination.id;
console.log(`[STEP 1] Termination Request Created. ID: ${terminationId}`);
const approvals = [
{ name: 'RBM Review', email: EMAILS.RBM },
{ name: 'ZBH Review', email: EMAILS.ZBH },
{ name: 'DD Lead Review', email: EMAILS.DD_LEAD },
{ name: 'Legal Verification', email: EMAILS.LEGAL },
{ name: 'NBH Evaluation', email: EMAILS.NBH },
{ name: 'SCN Issued', email: EMAILS.NBH },
{ name: 'Personal Hearing Outcome', email: EMAILS.DD_LEAD },
{ name: 'NBH Final Approval', email: EMAILS.NBH },
{ name: 'CCO Approval', email: EMAILS.CCO },
{ name: 'CEO Final Approval', email: EMAILS.CEO },
{ name: 'Legal Termination Letter', email: EMAILS.LEGAL }
];
let currentStep = 2;
for (const actor of approvals) {
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`);
const token = await login(actor.email);
await apiRequest(`/termination/${terminationId}/status`, 'PUT', {
action: 'approve',
remarks: `${actor.name} verification completed.`
}, token);
console.log(`[STEP ${currentStep}] ${actor.name} Result: SUCCESS`);
currentStep++;
await delay(500);
}
console.log('[FINAL STEP] Verifying Terminated Status...');
const finalDetails = await apiRequest(`/termination/${terminationId}`, 'GET', null, adminToken);
console.log(`Final Stage REACHED: ${finalDetails.termination.currentStage}`);
console.log('\n--- VERIFICATION SUCCESSFUL ---');
console.log('Role Participation: LEGAL TEAM, CCO, and CEO roles verified.');
console.log('Outcome: DEALER TERMINATED SUCCESSFULLY');
process.exit(0);
} catch (error) {
console.error('Workflow failed:', error.message);
process.exit(1);
}
}
run();

View File

@ -3,6 +3,7 @@
* This script automates the entire journey from Application to LOA. * This script automates the entire journey from Application to LOA.
*/ */
import fs from 'fs';
const BASE_URL = 'http://localhost:5000/api'; const BASE_URL = 'http://localhost:5000/api';
const PASSWORD = 'Admin@123'; const PASSWORD = 'Admin@123';
const OTP = '123456'; const OTP = '123456';
@ -16,6 +17,7 @@ const EMAILS = {
RBM_L1: 'rbm.ncr@royalenfield.com', RBM_L1: 'rbm.ncr@royalenfield.com',
ZM_L1: 'zm.ncr@royalenfield.com', ZM_L1: 'zm.ncr@royalenfield.com',
DD_LEAD: 'ddlead@royalenfield.com', DD_LEAD: 'ddlead@royalenfield.com',
ZBH: 'yashwin@gmail.com',
NBH: 'nbh@royalenfield.com', NBH: 'nbh@royalenfield.com',
DD_HEAD: 'ddhead@royalenfield.com', DD_HEAD: 'ddhead@royalenfield.com',
FDD: 'fdd@royalenfield.com', FDD: 'fdd@royalenfield.com',
@ -90,6 +92,25 @@ async function prospectLogin(phone) {
return data.data.token; // Prospect OTP returns token inside data object return data.data.token; // Prospect OTP returns token inside data object
} }
async function mockUploadDocument(appId, token, docType) {
const formData = new FormData();
const fileBuffer = fs.readFileSync('/home/laxman-h/Pictures/Screenshots/Screenshot from 2026-03-27 09-48-22.png');
const blob = new Blob([fileBuffer], { type: 'image/png' });
formData.append('file', blob, 'screenshot.png');
formData.append('documentType', docType);
const headers = { 'Authorization': `Bearer ${token}` };
const response = await fetch(`${BASE_URL}/onboarding/applications/${appId}/documents`, {
method: 'POST',
headers,
body: formData
});
if (!response.ok) {
throw new Error(`Upload Failed: ${response.status}`);
}
return response.json();
}
/** /**
* MAIN WORKFLOW * MAIN WORKFLOW
*/ */
@ -129,6 +150,7 @@ async function triggerWorkflow() {
const leadToken = await login(EMAILS.DD_LEAD); const leadToken = await login(EMAILS.DD_LEAD);
const rbmUser = users.data.find(u => u.email === EMAILS.RBM_L1); const rbmUser = users.data.find(u => u.email === EMAILS.RBM_L1);
const zmUser = users.data.find(u => u.email === EMAILS.ZM_L1); const zmUser = users.data.find(u => u.email === EMAILS.ZM_L1);
const zbhUser = users.data.find(u => u.email === EMAILS.ZBH) || users.data[2];
const intvResponse = await apiRequest('/assessment/interviews', 'POST', { const intvResponse = await apiRequest('/assessment/interviews', 'POST', {
applicationId: applicationUUID, applicationId: applicationUUID,
@ -145,21 +167,21 @@ async function triggerWorkflow() {
// FEEDBACK RBM // FEEDBACK RBM
log(4.1, 'RBM Giving Feedback...'); log(4.1, 'RBM Giving Feedback...');
const rbmToken = await login(EMAILS.RBM_L1); const rbmToken = await login(EMAILS.RBM_L1);
await apiRequest(`/assessment/interviews/${interviewId}/evaluation`, 'POST', { await apiRequest('/assessment/kt-matrix', 'POST', {
ktScore: 85, interviewId,
criteriaScores: [{ criterionName: 'Business Acumen', score: 8.5, maxScore: 10, weightage: 100 }],
feedback: 'Strong business acumen.', feedback: 'Strong business acumen.',
recommendation: 'Selected', recommendation: 'Selected'
status: 'Completed'
}, rbmToken); }, rbmToken);
// FEEDBACK ZM // FEEDBACK ZM
log(4.2, 'ZM Giving Feedback...'); log(4.2, 'ZM Giving Feedback...');
const zmToken = await login(EMAILS.ZM_L1); const zmToken = await login(EMAILS.ZM_L1);
await apiRequest(`/assessment/interviews/${interviewId}/evaluation`, 'POST', { await apiRequest('/assessment/kt-matrix', 'POST', {
ktScore: 90, interviewId,
criteriaScores: [{ criterionName: 'Vision', score: 9.0, maxScore: 10, weightage: 100 }],
feedback: 'Good vision for RE brand.', feedback: 'Good vision for RE brand.',
recommendation: 'Selected', recommendation: 'Selected'
status: 'Completed'
}, zmToken); }, zmToken);
// ZM DECISION (Rajesh Khanna) // ZM DECISION (Rajesh Khanna)
@ -174,24 +196,42 @@ async function triggerWorkflow() {
// 5. LEVEL-2 INTERVIEW // 5. LEVEL-2 INTERVIEW
log(5, 'Scheduling Level 2 Interview...'); log(5, 'Scheduling Level 2 Interview...');
const intv2Response = await apiRequest('/assessment/interviews', 'POST', { const intv2Response = await apiRequest('/assessment/interviews', 'POST', {
applicationId: applicationUUID, applicationId: applicationUUID,
level: 2, level: 2,
scheduledAt: new Date(Date.now() + 172800000).toISOString(), scheduledAt: new Date(Date.now() + 172800000).toISOString(),
type: 'Online', type: 'Online',
location: 'Teams', location: 'Teams',
participants: [ddLead.id] participants: [ddLead.id, zbhUser.id]
}, leadToken); }, leadToken);
const interviewId2 = intv2Response.data.id; const interviewId2 = intv2Response.data.id;
log(5.1, 'DD-Lead Giving Feedback...'); log(5.1, 'DD-Lead Giving Feedback...');
await apiRequest(`/assessment/interviews/${interviewId2}/evaluation`, 'POST', { await apiRequest('/assessment/level2-feedback', 'POST', {
ktScore: 95, interviewId: interviewId2,
feedback: 'Excellent profile.', overallScore: 9.5,
recommendation: 'Selected', feedbackItems: [
status: 'Completed' { type: 'Strategic Vision', comments: 'Excellent strategic planning.' },
{ type: 'Management Capabilities', comments: 'Strong team leadership.' },
{ type: 'Operational Understanding', comments: 'Knows the local market well.' }
],
recommendation: 'Selected'
}, leadToken); }, leadToken);
log(5.15, 'ZBH Giving Feedback...');
const zbhToken = await login(zbhUser.email);
await apiRequest('/assessment/level2-feedback', 'POST', {
interviewId: interviewId2,
overallScore: 9.0,
feedbackItems: [
{ type: 'Strategic Vision', comments: 'Good alignment with brand.' },
{ type: 'Key Strengths', comments: 'Great location proposed.' },
{ type: 'Areas of Concern', comments: 'None at this time.' }
],
recommendation: 'Selected'
}, zbhToken);
log(5.2, 'DD-Lead Finalizing Level 2 Decision...'); log(5.2, 'DD-Lead Finalizing Level 2 Decision...');
await apiRequest('/assessment/decision', 'POST', { await apiRequest('/assessment/decision', 'POST', {
interviewId: interviewId2, interviewId: interviewId2,
@ -218,15 +258,29 @@ async function triggerWorkflow() {
log(6.1, 'NBH Giving Feedback...'); log(6.1, 'NBH Giving Feedback...');
const nbhToken = await login(EMAILS.NBH); const nbhToken = await login(EMAILS.NBH);
await apiRequest(`/assessment/interviews/${interviewId3}/evaluation`, 'POST', { await apiRequest('/assessment/level2-feedback', 'POST', {
ktScore: 100, interviewId: interviewId3,
feedback: 'Highly recommended.', overallScore: 10,
recommendation: 'Selected', feedbackItems: [
status: 'Completed' { type: 'Business Vision & Strategy', comments: 'Highly recommended for this market.' },
{ type: 'Leadership & Decision Making', comments: 'Shows great potential.' }
],
recommendation: 'Selected'
}, nbhToken); }, nbhToken);
log(6.2, 'Head Finalizing Level 3 Decision...'); log(6.15, 'DD-Head Giving Feedback...');
const headToken = await login(EMAILS.DD_HEAD); const headToken = await login(EMAILS.DD_HEAD);
await apiRequest('/assessment/level2-feedback', 'POST', {
interviewId: interviewId3,
overallScore: 9.5,
feedbackItems: [
{ type: 'Operational & Financial Readiness', comments: 'Financially sound.' },
{ type: 'Brand Alignment', comments: 'Understands Royal Enfield ethos perfectly.' }
],
recommendation: 'Selected'
}, headToken);
log(6.2, 'Head Finalizing Level 3 Decision...');
await apiRequest('/assessment/decision', 'POST', { await apiRequest('/assessment/decision', 'POST', {
interviewId: interviewId3, interviewId: interviewId3,
decision: 'Approved', decision: 'Approved',
@ -248,7 +302,7 @@ async function triggerWorkflow() {
// 7. FDD MILESTONE // 7. FDD MILESTONE
log(7, 'FDD Agency Discovery & Report Upload...'); log(7, 'FDD Agency Discovery & Report Upload...');
const fddToken = await login(EMAILS.FDD); const fddToken = await login(EMAILS.FDD);
// FETCH ASSIGNMENT ID // FETCH ASSIGNMENT ID
const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken); const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
const assignmentId = assignmentRes.data.id; const assignmentId = assignmentRes.data.id;
@ -270,9 +324,41 @@ async function triggerWorkflow() {
log(7, 'FDD Milestone Complete.'); log(7, 'FDD Milestone Complete.');
await delay(); await delay();
// 8. PAYMENT GATE log(7.4, 'Uploading mandatory documents prior to LOI generation...');
log(8, 'Prospect Uploading Payment Receipt (Mock)...'); const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
// In real use, this is a multipart upload. Here we simulate the record update. for (const doc of requiredDocs) {
await mockUploadDocument(applicationUUID, adminToken, doc);
}
await delay(1000);
// 7.5 LOI APPROVAL
log(7.5, 'LOI Generation & Approval...');
const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
const loiRequestId = loiRes.data.id;
// Head Approval
await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
action: 'Approved',
remarks: 'Head Authorization for LOI'
}, headToken);
// NBH Approval
await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
action: 'Approved',
remarks: 'NBH Authorization for LOI'
}, nbhToken);
log(7.5, 'LOI Milestone Complete.');
await delay();
// 8. GENERATE DEALER CODES (Sequence: Post-LOI, Pre-LOA)
log(8, 'Admin Generating SAP Dealer Codes...');
await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
log(8, 'Dealer Codes Generated.');
await delay();
// 9. PAYMENT GATE
log(9, 'Prospect Uploading Payment Receipt (Mock)...');
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,
@ -281,9 +367,9 @@ async function triggerWorkflow() {
depositType: 'INITIAL', depositType: 'INITIAL',
status: 'Verified' status: 'Verified'
}, financeToken); }, financeToken);
log(8, 'Initial Security Deposit Verified.'); log(9, 'Initial Security Deposit Verified.');
log(8.1, 'Finance Verifying FINAL Security Deposit (₹15L)...'); log(9.1, 'Finance Verifying FINAL Security Deposit (₹15L)...');
await apiRequest('/loa/security-deposit', 'POST', { await apiRequest('/loa/security-deposit', 'POST', {
applicationId: applicationUUID, applicationId: applicationUUID,
amount: 1500000, amount: 1500000,
@ -291,12 +377,11 @@ async function triggerWorkflow() {
depositType: 'FINAL', depositType: 'FINAL',
status: 'Verified' status: 'Verified'
}, financeToken); }, financeToken);
log(8.1, 'Final Security Deposit Verified.'); log(9.1, 'Final Security Deposit Verified.');
await delay(); await delay();
// 9. FINAL LOA APPROVAL // 10. FINAL LOA APPROVAL
log(9, 'NBH & Head Approving Final LOA...'); log(10, 'NBH & Head Approving Final LOA...');
// Trigger LOA Request
const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken); const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
loaRequestId = loaRes.data.id; loaRequestId = loaRes.data.id;
@ -309,9 +394,25 @@ async function triggerWorkflow() {
action: 'Approved', action: 'Approved',
remarks: 'NBH Approval (Level 2)' remarks: 'NBH Approval (Level 2)'
}, nbhToken); }, nbhToken);
log(10, 'LOA Fully Approved.');
await delay();
log(9, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---'); // 11. MOVE TO INAUGURATION / APPROVED (Manual Transition)
log(9, `The application ${applicationId} is now at 'EOR Work' stage.`); log(11, 'Admin Moving Application to Approved stage for final onboarding...');
await apiRequest(`/onboarding/applications/${applicationUUID}/status`, 'PUT', {
status: 'Approved',
stage: 'Inauguration',
reason: 'Pre-onboarding verification complete'
}, adminToken);
log(11, 'Application is now in Approved status.');
await delay();
// 12. FINAL ONBOARDING
log(12, 'Admin Finalizing Dealer Onboarding...');
await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
log(12, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
log(12, `The application ${applicationId} is now at 'ONBOARDED' status.`);
} }
/** /**