added end to end testing files for all modules all midules coverd partially f&F resignation coverd majorlj
This commit is contained in:
parent
f70d4e7439
commit
bd7bdef46f
@ -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({
|
||||||
|
|||||||
50
src/common/utils/nomenclature.ts
Normal file
50
src/common/utils/nomenclature.ts
Normal 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)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 } });
|
||||||
|
|||||||
@ -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);
|
||||||
requestId,
|
|
||||||
|
let docId = null;
|
||||||
|
if (reqRecord) {
|
||||||
|
const docRecord = await db.OnboardingDocument.create({
|
||||||
|
applicationId: reqRecord.applicationId,
|
||||||
documentType: 'LOI',
|
documentType: 'LOI',
|
||||||
fileName: mockFile,
|
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
|
// Bridge: Transition from LOI Issued -> Dealer Code Generation
|
||||||
const request = await LoiRequest.findByPk(requestId);
|
const request = await LoiRequest.findByPk(requestId);
|
||||||
|
|||||||
@ -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' });
|
||||||
|
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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'] }] }
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
109
trigger-constitutional.js
Normal 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
118
trigger-resignation.js
Normal 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
109
trigger-termination.js
Normal 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();
|
||||||
@ -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',
|
||||||
@ -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.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user