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: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
{ 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) {
@ -107,7 +110,10 @@ async function seed() {
{ email: 'admin@royalenfield.com', name: 'Laxman H', role: 'Super Admin' },
{ email: 'lince@gmail.com', name: 'Lince', role: 'DD Admin' },
{ 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) {
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;
clearedAt: Date | null;
documentId: string | null; // For NOC upload
supportingDocument: string | null;
}
export interface FffClearanceInstance extends Model<FffClearanceAttributes>, FffClearanceAttributes { }
@ -55,6 +56,10 @@ export default (sequelize: Sequelize) => {
documentId: {
type: DataTypes.UUID,
allowNull: true
},
supportingDocument: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'fff_clearances',

View File

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

View File

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

View File

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

View File

@ -52,10 +52,24 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
// Find existing dealer or auto-detect dealer code
let targetDealerCodeId = dealerCodeId;
if (!targetDealerCodeId) {
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId } });
if (dealerCodeRecord) {
targetDealerCodeId = dealerCodeRecord.id;
let dealerCodeRecord = await DealerCode.findOne({ where: { applicationId } });
// 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 } });

View File

@ -229,11 +229,17 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
// Trigger Mock Document Generation
const mockFile = `LOI_${request.id}.pdf`;
await LoiDocumentGenerated.create({
requestId: request.id,
const docRecord = await db.OnboardingDocument.create({
applicationId: request.applicationId,
documentType: 'LOI',
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)
@ -324,12 +330,25 @@ export const generateDocument = async (req: AuthRequest, res: Response) => {
const { requestId } = req.body;
// Mocking document generation
const mockFile = `LOI_MANUAL_${Date.now()}.pdf`;
const doc = await LoiDocumentGenerated.create({
requestId,
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}`
filePath: `/uploads/loi/${mockFile}`,
status: 'active'
});
docId = docRecord.id;
}
const doc = docId ? await LoiDocumentGenerated.create({
requestId,
documentId: docId,
version: '1.0'
}) : null;
// Bridge: Transition from LOI Issued -> Dealer Code Generation
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 { syncLocationManagers } from '../master/syncHierarchy.service.js';
import { WorkflowService } from '../../services/WorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
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;
// 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({
opportunityId: null, // De-coupled from Opportunity table as per user request
opportunityId: activeOpportunityId,
applicationId,
applicantName,
email,
@ -86,7 +104,8 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
state,
experienceYears,
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,
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
progressPercentage: isOpportunityAvailable ? 10 : 0,
@ -625,8 +644,7 @@ const assignStageEvaluators = async (appIdOrId: string) => {
if (user) nationalUsers[r] = user.id;
}
// LOI: Finance, DD Head, NBH
if (nationalUsers['Finance']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['Finance'], role: 'Finance' });
// LOI: DD Head, NBH (Finance removed per user requirement)
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' });

View File

@ -2,17 +2,17 @@ import { Response } from 'express';
import db from '../../database/models/index.js';
const { ConstitutionalChange, Outlet, User, Worknote } = db;
import { Op, Transaction } from 'sequelize';
import { v4 as uuidv4 } from 'uuid';
import { AuthRequest } from '../../types/express.types.js';
import { CONSTITUTIONAL_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
export const submitRequest = async (req: AuthRequest, res: Response) => {
try {
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
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
const metadata = {

View File

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

View File

@ -7,12 +7,9 @@ import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
// Generate unique resignation ID
const generateResignationId = async (): Promise<string> => {
const count = await db.Resignation.count();
return `RES-${String(count + 1).padStart(3, '0')}`;
};
// Removed generateResignationId and moved to NomenclatureService
// Create resignation request (Dealer only)
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' });
}
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({
resignationId,
outletId,
@ -51,6 +54,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
progressPercentage: ResignationWorkflowService.calculateProgress(RESIGNATION_STAGES.ASM),
submittedOn: new Date(),
documents: [],
departmentalClearances: initialClearances,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
@ -111,7 +115,19 @@ export const getResignationById = async (req: AuthRequest, res: Response, next:
include: [
{ model: db.Outlet, as: 'outlet' },
{ 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({
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) {
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 dealerProfileId = (resignation as any).dealer?.dealerId;
const fnf = await db.FnF.create({
resignationId: resignation.id, outletId: resignation.outletId, dealerId: resignation.dealerId,
status: 'Initiated', totalReceivables: sapDues.data.outstandingInvoices, totalPayables: sapDues.data.securityDeposit,
resignationId: resignation.id,
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
}, { transaction });
@ -349,7 +374,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
if (!req.user) throw new Error('Unauthorized');
const { id } = req.params;
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 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' });
}
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({
departmentalClearances: clearances,
timeline: [...resignation.timeline, {
stage: resignation.currentStage,
timestamp: new Date(),
user: req.user.fullName,
action: cleared ? `Cleared ${department}` : `Revoked ${department} clearance`,
action: `Updated clearance for ${department}: ${status}`,
remarks
}]
}, { 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();
res.json({ success: true, message: `Clearance updated for ${department}`, resignation });
} catch (error) {

View File

@ -3,6 +3,8 @@ const router = express.Router();
import * as resignationController from './resignation.controller.js';
import { authenticate } from '../../common/middleware/auth.js';
import { uploadSingle } from '../../common/middleware/upload.js';
// Protected routes
router.post('/', authenticate as any, resignationController.createResignation);
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/withdraw', authenticate as any, resignationController.withdrawResignation);
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;

View File

@ -98,7 +98,15 @@ export const getFnFById = async (req: Request, res: Response) => {
include: [
{ model: Resignation, as: 'resignation' },
{ 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: 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 { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
import { NomenclatureService } from '../../common/utils/nomenclature.js';
// Create termination request
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');
const { dealerId, category, reason, proposedLwd, comments } = req.body;
const requestId = NomenclatureService.generateTerminationId();
const termination = await db.TerminationRequest.create({
requestId,
dealerId,
category,
reason,
@ -65,7 +68,13 @@ export const getTerminations = async (req: AuthRequest, res: Response, next: Nex
const terminations = await db.TerminationRequest.findAll({
where,
include: [{ model: db.Dealer, as: 'dealer' }],
include: [
{
model: db.Dealer,
as: 'dealer',
include: [{ model: db.DealerCode, as: 'dealerCode' }]
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, terminations });
@ -81,8 +90,26 @@ export const getTerminationById = async (req: AuthRequest, res: Response, next:
const { id } = req.params;
const termination = await db.TerminationRequest.findByPk(id, {
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.TerminationDocument,
as: 'uploadedDocuments',
include: [{ model: db.User, as: 'uploader', attributes: ['fullName'] }]
},
{ 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/relocations', relocationRoutes);
app.use('/api/outlets', outletRoutes);
app.use('/api/dealers', dealerRoutes);
app.use('/api/finance', settlementRoutes);
app.use('/api/worknotes', collaborationRoutes);
@ -163,11 +164,13 @@ const startServer = async () => {
await db.sequelize.authenticate();
logger.info('Database connection established successfully');
/*
// Sync database (in development only)
if (process.env.NODE_ENV === 'development') {
await db.sequelize.sync({ alter: false });
await db.sequelize.sync();
logger.info('Database models synchronized');
}
*/
// Start server
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.
*/
import fs from 'fs';
const BASE_URL = 'http://localhost:5000/api';
const PASSWORD = 'Admin@123';
const OTP = '123456';
@ -16,6 +17,7 @@ const EMAILS = {
RBM_L1: 'rbm.ncr@royalenfield.com',
ZM_L1: 'zm.ncr@royalenfield.com',
DD_LEAD: 'ddlead@royalenfield.com',
ZBH: 'yashwin@gmail.com',
NBH: 'nbh@royalenfield.com',
DD_HEAD: 'ddhead@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
}
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
*/
@ -129,6 +150,7 @@ async function triggerWorkflow() {
const leadToken = await login(EMAILS.DD_LEAD);
const rbmUser = users.data.find(u => u.email === EMAILS.RBM_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', {
applicationId: applicationUUID,
@ -145,21 +167,21 @@ async function triggerWorkflow() {
// FEEDBACK RBM
log(4.1, 'RBM Giving Feedback...');
const rbmToken = await login(EMAILS.RBM_L1);
await apiRequest(`/assessment/interviews/${interviewId}/evaluation`, 'POST', {
ktScore: 85,
await apiRequest('/assessment/kt-matrix', 'POST', {
interviewId,
criteriaScores: [{ criterionName: 'Business Acumen', score: 8.5, maxScore: 10, weightage: 100 }],
feedback: 'Strong business acumen.',
recommendation: 'Selected',
status: 'Completed'
recommendation: 'Selected'
}, rbmToken);
// FEEDBACK ZM
log(4.2, 'ZM Giving Feedback...');
const zmToken = await login(EMAILS.ZM_L1);
await apiRequest(`/assessment/interviews/${interviewId}/evaluation`, 'POST', {
ktScore: 90,
await apiRequest('/assessment/kt-matrix', 'POST', {
interviewId,
criteriaScores: [{ criterionName: 'Vision', score: 9.0, maxScore: 10, weightage: 100 }],
feedback: 'Good vision for RE brand.',
recommendation: 'Selected',
status: 'Completed'
recommendation: 'Selected'
}, zmToken);
// ZM DECISION (Rajesh Khanna)
@ -174,24 +196,42 @@ async function triggerWorkflow() {
// 5. LEVEL-2 INTERVIEW
log(5, 'Scheduling Level 2 Interview...');
const intv2Response = await apiRequest('/assessment/interviews', 'POST', {
applicationId: applicationUUID,
level: 2,
scheduledAt: new Date(Date.now() + 172800000).toISOString(),
type: 'Online',
location: 'Teams',
participants: [ddLead.id]
participants: [ddLead.id, zbhUser.id]
}, leadToken);
const interviewId2 = intv2Response.data.id;
log(5.1, 'DD-Lead Giving Feedback...');
await apiRequest(`/assessment/interviews/${interviewId2}/evaluation`, 'POST', {
ktScore: 95,
feedback: 'Excellent profile.',
recommendation: 'Selected',
status: 'Completed'
await apiRequest('/assessment/level2-feedback', 'POST', {
interviewId: interviewId2,
overallScore: 9.5,
feedbackItems: [
{ 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);
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...');
await apiRequest('/assessment/decision', 'POST', {
interviewId: interviewId2,
@ -218,15 +258,29 @@ async function triggerWorkflow() {
log(6.1, 'NBH Giving Feedback...');
const nbhToken = await login(EMAILS.NBH);
await apiRequest(`/assessment/interviews/${interviewId3}/evaluation`, 'POST', {
ktScore: 100,
feedback: 'Highly recommended.',
recommendation: 'Selected',
status: 'Completed'
await apiRequest('/assessment/level2-feedback', 'POST', {
interviewId: interviewId3,
overallScore: 10,
feedbackItems: [
{ type: 'Business Vision & Strategy', comments: 'Highly recommended for this market.' },
{ type: 'Leadership & Decision Making', comments: 'Shows great potential.' }
],
recommendation: 'Selected'
}, nbhToken);
log(6.2, 'Head Finalizing Level 3 Decision...');
log(6.15, 'DD-Head Giving Feedback...');
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', {
interviewId: interviewId3,
decision: 'Approved',
@ -270,9 +324,41 @@ async function triggerWorkflow() {
log(7, 'FDD Milestone Complete.');
await delay();
// 8. PAYMENT GATE
log(8, 'Prospect Uploading Payment Receipt (Mock)...');
// In real use, this is a multipart upload. Here we simulate the record update.
log(7.4, 'Uploading mandatory documents prior to LOI generation...');
const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
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);
await apiRequest('/loa/security-deposit', 'POST', {
applicationId: applicationUUID,
@ -281,9 +367,9 @@ async function triggerWorkflow() {
depositType: 'INITIAL',
status: 'Verified'
}, 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', {
applicationId: applicationUUID,
amount: 1500000,
@ -291,12 +377,11 @@ async function triggerWorkflow() {
depositType: 'FINAL',
status: 'Verified'
}, financeToken);
log(8.1, 'Final Security Deposit Verified.');
log(9.1, 'Final Security Deposit Verified.');
await delay();
// 9. FINAL LOA APPROVAL
log(9, 'NBH & Head Approving Final LOA...');
// Trigger LOA Request
// 10. FINAL LOA APPROVAL
log(10, 'NBH & Head Approving Final LOA...');
const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
loaRequestId = loaRes.data.id;
@ -309,9 +394,25 @@ async function triggerWorkflow() {
action: 'Approved',
remarks: 'NBH Approval (Level 2)'
}, nbhToken);
log(10, 'LOA Fully Approved.');
await delay();
log(9, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
log(9, `The application ${applicationId} is now at 'EOR Work' stage.`);
// 11. MOVE TO INAUGURATION / APPROVED (Manual Transition)
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.`);
}
/**