master page modified and trying to cover all models necessary flow

This commit is contained in:
laxmanhalaki 2026-03-18 19:51:10 +05:30
parent 92169b0fba
commit c43f86253b
17 changed files with 899 additions and 103 deletions

View File

@ -108,6 +108,7 @@ export const RESIGNATION_STAGES = {
ASM: 'ASM',
RBM: 'RBM',
ZBH: 'ZBH',
DD_LEAD: 'DD Lead',
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL: 'Legal',

View File

@ -0,0 +1,61 @@
import { Response, NextFunction } from 'express';
import db from '../../database/models/index.js';
import { AuthRequest } from '../../types/express.types.js';
/**
* Middleware to check if the user has the correct role for the current approval level of an LOI/LOA request.
*/
export const checkApprovalPermission = (type: 'LOI' | 'LOA') => {
return async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { requestId } = req.params;
const model = type === 'LOI' ? db.LoiApproval : db.LoaApproval;
// Find current pending approval level
const currentApproval = await model.findOne({
where: { requestId, action: 'Pending' },
order: [['level', 'ASC']]
});
if (!currentApproval) {
return res.status(404).json({ success: false, message: `No pending ${type} approval found` });
}
// Check if user's role matches the required role for this level
if (req.user?.roleCode !== currentApproval.approverRole && req.user?.roleCode !== 'Super Admin') {
return res.status(403).json({
success: false,
message: `Permission denied. Only ${currentApproval.approverRole} can perform this action at Level ${currentApproval.level}.`
});
}
next();
} catch (error) {
console.error('Approval permission check error:', error);
res.status(500).json({ success: false, message: 'Internal server error during permission check' });
}
};
};
/**
* Middleware to check if the user has the correct role for a specific application stage.
*/
export const checkStagePermission = (allowedRoles: string[]) => {
return async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
if (!allowedRoles.includes(req.user.roleCode) && req.user.roleCode !== 'Super Admin') {
return res.status(403).json({
success: false,
message: 'Access denied for this stage action',
requiredRoles: allowedRoles
});
}
next();
} catch (error) {
res.status(500).json({ success: false, message: 'Stage permission check failed' });
}
};
};

View File

@ -0,0 +1,115 @@
import { v4 as uuidv4 } from 'uuid';
/**
* External Mocks Service
* Centralized place for mocking external dependencies until real APIs are ready.
*/
export const ExternalMocksService = {
/**
* Mock SAP Dealer Code Generation
* Returns random codes for Sales, Service, GMA, and Gear.
*/
mockGenerateSapCodes: async (applicationId: string) => {
console.log(`[MOCK SAP] Generating codes for application: ${applicationId}`);
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000));
return {
success: true,
data: {
salesCode: `SLS-${Math.floor(1000 + Math.random() * 9000)}`,
serviceCode: `SRV-${Math.floor(1000 + Math.random() * 9000)}`,
gmaCode: `GMA-${Math.floor(1000 + Math.random() * 9000)}`,
gearCode: `GER-${Math.floor(1000 + Math.random() * 9000)}`,
sapMasterId: uuidv4().substring(0, 8).toUpperCase()
}
};
},
/**
* Mock Gemini AI Summary Generation
* Generates a consensus-based summary from panel feedback.
*/
mockGenerateAiSummary: async (applicationId: string, feedbackList: any[]) => {
console.log(`[MOCK GEMINI] Generating AI summary for: ${applicationId}`);
await new Promise(resolve => setTimeout(resolve, 1500));
const approveCount = feedbackList.filter(f => f.recommendation === 'Approve' || f.recommendation === 'Selected').length;
const total = feedbackList.length;
let summary = "";
if (approveCount === total) {
summary = "The panel has reached a strong consensus for approval. The candidate demonstrates exceptional business acumen and brand passion, with zero identified risks.";
} else if (approveCount > total / 2) {
summary = "The majority of the panel recommends approval, highlighting strong local market knowledge, though some concerns were raised regarding initial working capital which should be monitored.";
} else {
summary = "The panel is divided or leaning towards rejection. Concerns primarily focus on inadequate infrastructure readiness and limited previous experience in the automotive sector.";
}
return {
success: true,
summary
};
},
/**
* Mock Google Calendar Invite
* Returns a mock meeting link.
*/
mockScheduleMeeting: async (details: any) => {
console.log(`[MOCK GOOGLE CALENDAR] Scheduling meeting: ${details.type} at ${details.scheduledAt}`);
return {
success: true,
meetLink: `https://meet.google.com/mock-${uuidv4().substring(0, 8)}`,
calendarEventId: uuidv4()
};
},
/**
* Mock WhatsApp Notification
* Logs the notification instead of sending.
*/
mockSendWhatsApp: async (phone: string, message: string) => {
console.log(`[MOCK WHATSAPP] To: ${phone} | Message: ${message}`);
// In a real implementation, sensitive docs would be excluded here per SRS.
return {
success: true,
messageId: `WA-${uuidv4().substring(0, 12)}`
};
},
/**
* Mock SAP Status Synchronization
* Simulates updating dealer status in SAP.
*/
mockSyncDealerStatusToSap: async (dealerCode: string, status: string) => {
console.log(`[MOCK SAP] Syncing status for dealer ${dealerCode}: ${status}`);
await new Promise(resolve => setTimeout(resolve, 1200));
return {
success: true,
sapTransactionId: `SAP-TX-${uuidv4().substring(0, 8).toUpperCase()}`,
timestamp: new Date().toISOString()
};
},
/**
* Mock SAP Financial Data Retrieval
* Simulates fetching outstanding dues and credit limits from SAP.
*/
mockGetFinancialDuesFromSap: async (dealerCode: string) => {
console.log(`[MOCK SAP] Fetching financial dues for dealer: ${dealerCode}`);
await new Promise(resolve => setTimeout(resolve, 1500));
return {
success: true,
data: {
outstandingInvoices: Math.floor(50000 + Math.random() * 200000),
securityDeposit: Math.floor(100000 + Math.random() * 500000),
creditLimit: Math.floor(1000000 + Math.random() * 5000000),
pendingClaims: Math.floor(10000 + Math.random() * 50000)
}
};
}
};
export default ExternalMocksService;

View File

@ -33,11 +33,13 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
try {
const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }]
// Calculate score logic (Placeholder)
let calculatedScore = 0;
let totalWeight = 0;
const application = await db.Application.findByPk(applicationId);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
let totalWeightedScore = 0;
for (const resp of responses) {
// Save response
await QuestionnaireResponse.create({
applicationId,
questionnaireId,
@ -45,19 +47,56 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
responseValue: resp.responseValue,
attachmentUrl: resp.attachmentUrl
});
// Add scoring logic here based on question type/weight
// Scoring Logic:
// 1. Fetch the question to get its weight
const question = await QuestionnaireQuestion.findByPk(resp.questionId, {
include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }]
});
if (question) {
let questionScore = 0;
// If it's an option-based question, find the selected option's score
if (question.questionOptions && question.questionOptions.length > 0) {
const selectedOption = question.questionOptions.find((opt: any) => opt.optionText === resp.responseValue);
if (selectedOption) {
questionScore = selectedOption.score;
}
} else if (!isNaN(Number(resp.responseValue))) {
// If it's a numeric input, use it directly (if appropriate)
questionScore = Number(resp.responseValue);
}
totalWeightedScore += (questionScore * (question.weight || 1));
}
}
// Create Score Record
await QuestionnaireScore.create({
// Create/Update Score Record
await QuestionnaireScore.upsert({
applicationId,
questionnaireId,
score: calculatedScore,
maxScore: 100, // Placeholder
score: totalWeightedScore,
maxScore: 100, // Based on SRS section weightages
status: 'Completed'
});
res.status(201).json({ success: true, message: 'Responses submitted successfully' });
// Update Application
await application.update({
score: totalWeightedScore,
overallStatus: 'Questionnaire Completed',
progressPercentage: 20
});
// Log Status History
await db.ApplicationStatusHistory.create({
applicationId: application.id,
previousStatus: 'Questionnaire Pending',
newStatus: 'Questionnaire Completed',
changedBy: req.user?.id || null,
reason: 'Questionnaire submitted by applicant'
});
res.status(201).json({ success: true, message: 'Responses submitted and scored successfully', score: totalWeightedScore });
} catch (error) {
console.error('Submit response error:', error);
res.status(500).json({ success: false, message: 'Error submitting responses' });
@ -99,6 +138,24 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
await db.Application.update({ overallStatus: newStatus }, { where: { id: applicationId } });
// MOCK INTEGRATIONS
// 1. Google Calendar Mock
const { meetLink } = await ExternalMocksService.mockScheduleMeeting({
type,
scheduledAt,
applicationId
});
await interview.update({ linkOrLocation: meetLink });
// 2. WhatsApp Mock
const appRecord = await db.Application.findByPk(applicationId);
if (appRecord) {
await ExternalMocksService.mockSendWhatsApp(
appRecord.phone,
`Dear ${appRecord.applicantName}, your ${type} is scheduled at ${scheduledAt}. Join here: ${meetLink}`
);
}
if (participants && participants.length > 0) {
console.log(`Processing ${participants.length} participants...`);
for (const userId of participants) {
@ -316,6 +373,48 @@ export const submitLevel2Feedback = async (req: AuthRequest, res: Response) => {
// --- AI Summary ---
import { ExternalMocksService } from '../../common/utils/externalMocks.service.js';
export const generateAiSummary = async (req: AuthRequest, res: Response) => {
try {
const { applicationId } = req.params;
// 1. Fetch all interview evaluations for this application
const interviews = await Interview.findAll({
where: { applicationId },
include: [{ model: InterviewEvaluation, as: 'evaluations' }]
});
const allEvaluations = interviews.flatMap((i: any) => i.evaluations || []);
if (allEvaluations.length === 0) {
return res.status(400).json({ success: false, message: 'No interview evaluations found to summarize' });
}
// 2. Map evaluations to a format Gemini (mock) understands
const feedbackList = allEvaluations.map((e: any) => ({
recommendation: e.recommendation,
feedback: e.qualitativeFeedback
}));
// 3. Trigger Mock Gemini call
const { summary } = await ExternalMocksService.mockGenerateAiSummary(applicationId as string, feedbackList);
// 4. Save/Update AI Summary
const [aiSummary, created] = await AiSummary.upsert({
applicationId,
summary,
status: 'Generated',
modelUsed: 'Gemini 1.5 Pro (Mock)'
}, { returning: true });
res.json({ success: true, data: aiSummary });
} catch (error) {
console.error('Generate AI summary error:', error);
res.status(500).json({ success: false, message: 'Error generating AI summary' });
}
};
export const getAiSummary = async (req: Request, res: Response) => {
try {
const { applicationId } = req.params;
@ -325,6 +424,10 @@ export const getAiSummary = async (req: Request, res: Response) => {
order: [['createdAt', 'DESC']]
});
if (!summary) {
return res.json({ success: false, message: 'No AI Summary generated yet' });
}
res.json({ success: true, data: summary });
} catch (error) {
console.error('Get AI summary error:', error);

View File

@ -20,6 +20,7 @@ router.post('/recommendation', assessmentController.updateRecommendation);
router.post('/decision', assessmentController.updateInterviewDecision);
// AI Summary
router.post('/ai-summary/:applicationId', assessmentController.generateAiSummary);
router.get('/ai-summary/:applicationId', assessmentController.getAiSummary);
export default router;

View File

@ -28,12 +28,44 @@ export const getChecklist = async (req: Request, res: Response) => {
export const createChecklist = async (req: AuthRequest, res: Response) => {
try {
const { applicationId } = req.body;
const checklist = await EorChecklist.create({ applicationId });
// Create Default Items?
// const defaultItems = [...];
// for...
res.status(201).json({ success: true, message: 'EOR Checklist initiated', data: checklist });
const application = await db.Application.findByPk(applicationId);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
const [checklist, created] = await EorChecklist.findOrCreate({
where: { applicationId },
defaults: { status: 'In Progress' }
});
if (created) {
// Define Default Mandatory Items per SRS
const defaultItems = [
{ itemType: 'Architecture', description: 'Brand Signage & Facade as per guidelines' },
{ itemType: 'Architecture', description: 'Interior Fit-out & Furniture' },
{ itemType: 'Sales', description: 'Display Vehicles (All models) available' },
{ itemType: 'Sales', description: 'Test Ride Vehicles registered' },
{ itemType: 'Training', description: 'Sales Staff (DSE) Training Completed' },
{ itemType: 'Training', description: 'Service Technician (Pro-Meck) Training' },
{ itemType: 'IT', description: 'DMS (Dealer Management System) configured' },
{ itemType: 'IT', description: 'High-speed internet & IT Hardware ready' },
{ itemType: 'Service', description: 'Special Tools & Equipment installed' },
{ itemType: 'Finance', description: 'Bank Account mapped in SAP' }
];
const itemsData = defaultItems.map(item => ({
...item,
checklistId: checklist.id,
isCompliant: false
}));
await EorChecklistItem.bulkCreate(itemsData);
}
await application.update({ overallStatus: 'EOR In Progress' });
res.status(201).json({ success: true, message: 'EOR Checklist initiated with default items', data: checklist });
} catch (error) {
console.error('Create EOR checklist error:', error);
res.status(500).json({ success: false, message: 'Error creating checklist' });
}
}

View File

@ -27,27 +27,113 @@ export const createRequest = async (req: AuthRequest, res: Response) => {
try {
const { applicationId } = req.body;
const request = await LoaRequest.create({
applicationId,
requestedBy: req.user?.id,
status: 'Pending'
const application = await db.Application.findByPk(applicationId);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
const [request, created] = await LoaRequest.findOrCreate({
where: { applicationId },
defaults: {
requestedBy: req.user?.id,
status: 'In Progress'
}
});
await LoaApproval.create({
requestId: request.id,
level: 1,
approverRole: 'Zone Manager', // Example
action: 'Pending'
// Initialize first level approval (DD Head) for LOA
await LoaApproval.findOrCreate({
where: { requestId: request.id, level: 1 },
defaults: {
approverRole: 'DD Head',
action: 'Pending'
}
});
res.status(201).json({ success: true, message: 'LOA Request created', data: request });
await application.update({ overallStatus: 'LOA Pending' });
res.status(201).json({ success: true, message: 'LOA Request initiated with DD Head approval', data: request });
} catch (error) {
console.error('Create LOA request error:', error);
res.status(500).json({ success: false, message: 'Error creating LOA request' });
}
};
// ... Similar approve/generate logic as LOI ...
export const approveRequest = async (req: AuthRequest, res: Response) => {
try {
const { requestId } = req.params;
const { action, remarks } = req.body;
const request = await LoaRequest.findByPk(requestId);
if (!request) return res.status(404).json({ success: false, message: 'LOA Request not found' });
const currentApproval = await LoaApproval.findOne({
where: { requestId, action: 'Pending' },
order: [['level', 'ASC']]
});
if (!currentApproval) return res.status(400).json({ success: false, message: 'No pending approval found' });
await currentApproval.update({
action,
remarks,
approverId: req.user?.id,
approvedAt: action === 'Approved' ? new Date() : null
});
if (action === 'Rejected') {
await request.update({ status: 'Rejected' });
await db.Application.update({ overallStatus: 'LOA Rejected' }, { where: { id: request.applicationId } });
return res.json({ success: true, message: 'LOA Request rejected' });
}
const nextLevelMap: any = {
1: { role: 'NBH', level: 2 },
2: { role: 'Final', level: 3 }
};
const next = nextLevelMap[currentApproval.level];
if (next && next.role !== 'Final') {
await LoaApproval.create({
requestId: request.id,
level: next.level,
approverRole: next.role,
action: 'Pending'
});
res.json({ success: true, message: `Approved at Level ${currentApproval.level}. Moved to ${next.role}` });
} else {
await request.update({ status: 'Approved', approvedBy: req.user?.id, approvedAt: new Date() });
const mockFile = `LOA_${request.id}.pdf`;
await LoaDocumentGenerated.create({
requestId: request.id,
documentType: 'LOA',
fileName: mockFile,
filePath: `/uploads/loa/${mockFile}`
});
await db.Application.update({ overallStatus: 'Authorized for Operations' }, { where: { id: request.applicationId } });
res.json({ success: true, message: 'LOA fully approved and issued' });
}
} catch (error) {
console.error('Approve LOA request error:', error);
res.status(500).json({ success: false, message: 'Error processing approval' });
}
};
export const generateDocument = async (req: AuthRequest, res: Response) => {
try {
const { requestId } = req.body;
const mockFile = `LOA_MANUAL_${Date.now()}.pdf`;
const doc = await LoaDocumentGenerated.create({
requestId,
documentType: 'LOA',
fileName: mockFile,
filePath: `/uploads/loa/${mockFile}`
});
res.json({ success: true, message: 'LOA Document generated (Mock)', data: doc });
} catch (error) {
res.status(500).json({ success: false, message: 'Error generating document' });
}
};
// --- Security Deposit ---

View File

@ -11,7 +11,8 @@ export const getRequest = async (req: Request, res: Response) => {
where: { applicationId },
include: [
{ model: LoiApproval, as: 'approvals' },
{ model: LoiDocumentGenerated, as: 'generatedDocuments' }
{ model: LoiDocumentGenerated, as: 'generatedDocuments' },
{ model: LoiAcknowledgement, as: 'acknowledgement' }
]
});
res.json({ success: true, data: request });
@ -21,26 +22,58 @@ export const getRequest = async (req: Request, res: Response) => {
}
};
export const acknowledgeRequest = async (req: AuthRequest, res: Response) => {
try {
const { requestId } = req.params;
const { documentId } = req.body;
const request = await LoiRequest.findByPk(requestId);
if (!request) return res.status(404).json({ success: false, message: 'LOI Request not found' });
await LoiAcknowledgement.create({
requestId,
applicationId: request.applicationId,
documentId,
acknowledgedAt: new Date(),
status: 'Acknowledged'
});
await db.Application.update({ overallStatus: 'Dealer Code Generation' }, { where: { id: request.applicationId } });
res.json({ success: true, message: 'LOI Acknowledged by applicant' });
} catch (error) {
console.error('LOI Acknowledge error:', error);
res.status(500).json({ success: false, message: 'Error acknowledging LOI' });
}
};
export const createRequest = async (req: AuthRequest, res: Response) => {
try {
const { applicationId } = req.body;
const request = await LoiRequest.create({
applicationId,
requestedBy: req.user?.id,
status: 'Pending'
const application = await db.Application.findByPk(applicationId);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
const [request, created] = await LoiRequest.findOrCreate({
where: { applicationId },
defaults: {
requestedBy: req.user?.id,
status: 'In Progress'
}
});
// Create Initial Approvals? (Or triggered by Workflow engine)
// Manual for now:
await LoiApproval.create({
requestId: request.id,
level: 1,
approverRole: 'Zone Manager', // Example
action: 'Pending'
// Initialize first level approval (Finance) if not already exists
await LoiApproval.findOrCreate({
where: { requestId: request.id, level: 1 },
defaults: {
approverRole: 'Finance',
action: 'Pending'
}
});
res.status(201).json({ success: true, message: 'LOI Request created', data: request });
await application.update({ overallStatus: 'LOI In Progress' });
res.status(201).json({ success: true, message: 'LOI Request initiated with Finance approval', data: request });
} catch (error) {
console.error('Create LOI request error:', error);
res.status(500).json({ success: false, message: 'Error creating LOI request' });
@ -52,27 +85,92 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
const { requestId } = req.params;
const { action, remarks } = req.body; // action: Approved/Rejected
// Find pending approval for this user/role (Simplified logic)
const approval = await LoiApproval.findOne({
where: { requestId, action: 'Pending' } // , level: 1 etc.
const request = await LoiRequest.findByPk(requestId);
if (!request) return res.status(404).json({ success: false, message: 'LOI Request not found' });
// Find current pending approval
const currentApproval = await LoiApproval.findOne({
where: { requestId, action: 'Pending' },
order: [['level', 'ASC']]
});
if (!approval) return res.status(404).json({ success: false, message: 'No pending approval found' });
await approval.update({
action,
remarks,
approverId: req.user?.id
});
// If Approved, check if next level needed or finalize
if (action === 'Approved') {
await LoiRequest.update({ status: 'Approved', approvedBy: req.user?.id, approvedAt: new Date() }, { where: { id: requestId } });
} else {
await LoiRequest.update({ status: 'Rejected' }, { where: { id: requestId } });
if (!currentApproval) {
return res.status(400).json({ success: false, message: 'No pending approval levels found' });
}
res.json({ success: true, message: 'LOI Request updated' });
// MANDATORY DOCUMENT CHECK (SRS Requirement)
// Level 2+ requires minimum set of documents uploaded by applicant
if (currentApproval.level === 1 && action === 'Approved') {
const docCount = await db.Document.count({
where: { requestId: request.applicationId, requestType: 'application' }
});
if (docCount < 5) { // SRS requires 18, using 5 for functional demo
return res.status(400).json({
success: false,
message: `Mandatory Document Check Failed: Applicant must upload at least 5 required documents (CIBIL, City Map, etc.) before DD Head approval. Current: ${docCount}`
});
}
}
// 1. Update current level
await currentApproval.update({
action,
remarks,
approverId: req.user?.id,
approvedAt: action === 'Approved' ? new Date() : null
});
// 2. Handle Logic based on Action
if (action === 'Rejected') {
await request.update({ status: 'Rejected' });
await db.Application.update({ overallStatus: 'LOI Rejected' }, { where: { id: request.applicationId } });
return res.json({ success: true, message: 'LOI Request rejected' });
}
// 3. If Approved, determine next step
const nextLevelMap: any = {
1: { role: 'DD Head', level: 2 },
2: { role: 'NBH', level: 3 },
3: { role: 'Final', level: 4 }
};
const next = nextLevelMap[currentApproval.level];
if (next && next.role !== 'Final') {
// Initiate next level
await LoiApproval.create({
requestId: request.id,
level: next.level,
approverRole: next.role,
action: 'Pending'
});
res.json({ success: true, message: `Approved at Level ${currentApproval.level}. Moved to ${next.role}` });
} else {
// Final Approval reached
await request.update({ status: 'Approved', approvedBy: req.user?.id, approvedAt: new Date() });
// Trigger Mock Document Generation
const mockFile = `LOI_${request.id}.pdf`;
await LoiDocumentGenerated.create({
requestId: request.id,
documentType: 'LOI',
fileName: mockFile,
filePath: `/uploads/loi/${mockFile}`
});
await db.Application.update({ overallStatus: 'LOI Issued' }, { where: { id: request.applicationId } });
res.json({ success: true, message: 'LOI Request fully approved and document generated' });
}
await AuditLog.create({
userId: req.user?.id,
action: action === 'Approved' ? AUDIT_ACTIONS.LOI_APPROVED : AUDIT_ACTIONS.LOI_REJECTED,
entityType: 'loi_request',
entityId: requestId,
newData: { level: currentApproval.level, action }
});
} catch (error) {
console.error('Approve LOI request error:', error);
res.status(500).json({ success: false, message: 'Error processing approval' });
@ -81,10 +179,24 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
export const generateDocument = async (req: AuthRequest, res: Response) => {
try {
// Logic to generate PDF from Template
// Create Document record
// Create LoiDocumentGenerated record
res.json({ success: true, message: 'LOI Document generated (Placeholder)' });
const { requestId } = req.body;
// Mocking document generation
const mockFile = `LOI_MANUAL_${Date.now()}.pdf`;
const doc = await LoiDocumentGenerated.create({
requestId,
documentType: 'LOI',
fileName: mockFile,
filePath: `/uploads/loi/${mockFile}`
});
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.LOI_GENERATED,
entityType: 'loi_request',
entityId: requestId
});
res.json({ success: true, message: 'LOI Document generated (Mock)', data: doc });
} catch (error) {
res.status(500).json({ success: false, message: 'Error generating document' });
}

View File

@ -8,6 +8,7 @@ router.use(authenticate as any);
router.get('/request/:applicationId', loiController.getRequest);
router.post('/request', loiController.createRequest);
router.post('/request/:requestId/approve', loiController.approveRequest);
router.post('/request/:requestId/acknowledge', loiController.acknowledgeRequest);
router.post('/request/:requestId/generate', loiController.generateDocument);
export default router;

View File

@ -42,6 +42,7 @@ router.get('/area-managers', masterController.getAreaManagers);
// Outlets
router.get('/outlets', outletController.getOutlets);
router.get('/outlets/code/:code', outletController.getOutletByCode);
router.get('/outlets/:id', outletController.getOutletById);
router.post('/outlets', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, outletController.createOutlet);
router.put('/outlets/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, outletController.updateOutlet);

View File

@ -187,3 +187,36 @@ export const updateOutlet = async (req: AuthRequest, res: Response) => {
});
}
};
// Get outlet by code
export const getOutletByCode = async (req: AuthRequest, res: Response) => {
try {
const { code } = req.params;
const outlet = await Outlet.findOne({
where: { code },
include: [{
model: User,
as: 'dealer',
attributes: ['name', 'email', 'phone']
}]
});
if (!outlet) {
return res.status(404).json({
success: false,
message: 'Outlet not found'
});
}
res.json({
success: true,
outlet
});
} catch (error) {
console.error('Get outlet by code error:', error);
res.status(500).json({
success: false,
message: 'Error fetching outlet'
});
}
};

View File

@ -353,46 +353,41 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
return res.status(400).json({ success: false, message: 'No applications selected' });
}
// assignedTo is expected to be an array of User IDs from frontend now
// But database only supports single UUID for assignedTo.
// Strategy: Assign the first user as primary assignee.
const primaryAssigneeId = Array.isArray(assignedTo) && assignedTo.length > 0 ? assignedTo[0] : null;
// Verify primaryAssigneeId is a valid UUID if strictly enforced by DB, but Sequelize might handle null.
// Update Applications
const updateData: any = {
ddLeadShortlisted: true,
overallStatus: 'Shortlisted',
updatedAt: new Date(),
};
if (primaryAssigneeId) {
updateData.assignedTo = primaryAssigneeId;
if (!assignedTo || !Array.isArray(assignedTo) || assignedTo.length === 0) {
return res.status(400).json({ success: false, message: 'At least one assignee (DD-ZM/RBM) is required' });
}
await Application.update(updateData, {
// Strategy: Assign the first user as primary assignee for the single FK field,
// but add ALL as participants to enforce dual-responsibility.
const primaryAssigneeId = assignedTo[0];
// Update Applications
await Application.update({
ddLeadShortlisted: true,
isShortlisted: true,
overallStatus: 'Shortlisted',
assignedTo: primaryAssigneeId,
updatedAt: new Date(),
}, {
where: {
id: { [Op.in]: applicationIds }
}
});
// Add participants
if (Array.isArray(assignedTo) && assignedTo.length > 0) {
for (const appId of applicationIds) {
for (const userId of assignedTo) {
await db.RequestParticipant.findOrCreate({
where: {
requestId: appId,
requestType: 'application',
userId,
participantType: 'assignee'
},
defaults: {
joinedMethod: 'auto'
}
});
}
// Add all assigned users as participants for each application
for (const appId of applicationIds) {
for (const userId of assignedTo) {
await db.RequestParticipant.findOrCreate({
where: {
requestId: appId,
requestType: 'application',
userId,
participantType: 'assignee'
},
defaults: {
joinedMethod: 'auto'
}
});
}
}
@ -402,23 +397,23 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
previousStatus: 'Questionnaire Completed',
newStatus: 'Shortlisted',
changedBy: req.user?.id,
reason: remarks ? `${remarks} (Assignees: ${Array.isArray(assignedTo) ? assignedTo.join(', ') : assignedTo})` : 'Bulk Shortlist'
reason: remarks || 'Bulk Shortlist'
}));
await ApplicationStatusHistory.bulkCreate(historyEntries);
// Audit Log
const auditEntries = applicationIds.map(appId => ({
userId: req.user?.id,
action: AUDIT_ACTIONS.UPDATED,
action: AUDIT_ACTIONS.SHORTLISTED,
entityType: 'application',
entityId: appId,
newData: { ddLeadShortlisted: true, assignedTo: primaryAssigneeId, remarks }
newData: { isShortlisted: true, assignedTo }
}));
await AuditLog.bulkCreate(auditEntries);
res.json({
success: true,
message: `Successfully shortlisted ${applicationIds.length} application(s)`
message: `Successfully shortlisted ${applicationIds.length} application(s) and assigned to ${assignedTo.length} users.`
});
} catch (error) {
console.error('Bulk shortlist error:', error);
@ -493,3 +488,42 @@ export const updateArchitectureStatus = async (req: AuthRequest, res: Response)
res.status(500).json({ success: false, message: 'Error updating architecture status' });
}
};
import { ExternalMocksService } from '../../common/utils/externalMocks.service.js';
export const generateDealerCodes = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params; // applicationId
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
// Trigger Mock SAP Integration
const { data: sapData } = await ExternalMocksService.mockGenerateSapCodes(application.applicationId);
// Save Dealer Codes
await db.DealerCode.create({
applicationId: id,
salesCode: sapData.salesCode,
serviceCode: sapData.serviceCode,
gmaCode: sapData.gmaCode,
gearCode: sapData.gearCode,
sapMasterId: sapData.sapMasterId,
status: 'Active'
});
await application.update({
overallStatus: 'Architecture Team Assigned',
progressPercentage: 80
});
res.json({
success: true,
message: 'SAP Dealer Codes generated successfully (Mock)',
data: sapData
});
} catch (error) {
console.error('Generate dealer code error:', error);
res.status(500).json({ success: false, message: 'Error generating dealer codes' });
}
};

View File

@ -3,7 +3,7 @@ const router = express.Router();
import {
submitApplication, getApplications, getApplicationById, updateApplicationStatus,
uploadDocuments, getApplicationDocuments, bulkShortlist,
assignArchitectureTeam, updateArchitectureStatus
assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes
} from './onboarding.controller.js';
import { authenticate } from '../../common/middleware/auth.js';
@ -26,6 +26,7 @@ router.get('/applications/:id/documents', getApplicationDocuments); // Existing
// Architecture-related routes
router.post('/applications/:id/assign-architecture', assignArchitectureTeam);
router.put('/applications/:id/architecture-status', updateArchitectureStatus);
router.post('/applications/:id/generate-codes', generateDealerCodes);
// Questionnaire Routes

View File

@ -4,6 +4,7 @@ import logger from '../../common/utils/logger.js';
import { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
import { Op, Transaction } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
// Generate unique resignation ID
const generateResignationId = async (): Promise<string> => {
@ -16,7 +17,8 @@ const calculateProgress = (stage: string): number => {
const stageProgress: Record<string, number> = {
[RESIGNATION_STAGES.ASM]: 15,
[RESIGNATION_STAGES.RBM]: 30,
[RESIGNATION_STAGES.ZBH]: 45,
[RESIGNATION_STAGES.ZBH]: 40,
[RESIGNATION_STAGES.DD_LEAD]: 50,
[RESIGNATION_STAGES.NBH]: 60,
[RESIGNATION_STAGES.DD_ADMIN]: 65,
[RESIGNATION_STAGES.LEGAL]: 70,
@ -260,7 +262,8 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
const stageFlow: Record<string, string> = {
[RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM,
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH,
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.DD_LEAD,
[RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.NBH,
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.SPARES_CLEARANCE,
@ -297,11 +300,52 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
timeline
}, { transaction });
// If completed, update outlet status
// If completed, update outlet status and sync with SAP
if (nextStage === RESIGNATION_STAGES.COMPLETED) {
await (resignation as any).outlet.update({
status: 'Closed'
}, { transaction });
// Trigger Mock SAP Sync
ExternalMocksService.mockSyncDealerStatusToSap((resignation as any).outlet.code, 'Inactive')
.catch(err => logger.error('Error syncing resignation completion to SAP:', err));
}
// If F&F Initiated, create F&F record and fetch mock SAP dues
if (nextStage === RESIGNATION_STAGES.FNF_INITIATED) {
const sapDues = await ExternalMocksService.mockGetFinancialDuesFromSap((resignation as any).outlet.code);
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,
netAmount: sapDues.data.securityDeposit - sapDues.data.outstandingInvoices
}, { transaction });
// Create initial line items from SAP data
await db.FnFLineItem.bulkCreate([
{
fnfId: fnf.id,
itemType: 'Receivable',
description: 'Outstanding Invoices from SAP',
department: 'Finance',
amount: sapDues.data.outstandingInvoices,
addedBy: req.user.id
},
{
fnfId: fnf.id,
itemType: 'Payable',
description: 'Security Deposit from SAP',
department: 'Finance',
amount: sapDues.data.securityDeposit,
addedBy: req.user.id
}
], { transaction });
logger.info(`F&F record and mock line items created for resignation: ${resignation.resignationId}`);
}
// Create audit log

View File

@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import db from '../../database/models/index.js';
const { FinancePayment, FnF, Application, Resignation, User, Outlet } = db;
const { FinancePayment, FnF, Application, Resignation, User, Outlet, FnFLineItem, TerminationRequest } = db;
import { AuthRequest } from '../../types/express.types.js';
export const getOnboardingPayments = async (req: Request, res: Response) => {
@ -28,7 +28,12 @@ export const getFnFSettlements = async (req: Request, res: Response) => {
{
model: Resignation,
as: 'resignation',
attributes: ['resignationId']
attributes: ['id', 'resignationId']
},
{
model: TerminationRequest,
as: 'terminationRequest',
attributes: ['id', 'status', 'category']
},
{
model: Outlet,
@ -36,8 +41,12 @@ export const getFnFSettlements = async (req: Request, res: Response) => {
include: [{
model: User,
as: 'dealer',
attributes: ['name']
attributes: ['name', 'id']
}]
},
{
model: FnFLineItem,
as: 'lineItems'
}
],
order: [['createdAt', 'DESC']]
@ -99,3 +108,114 @@ export const updateFnF = async (req: AuthRequest, res: Response) => {
res.status(500).json({ success: false, message: 'Error updating F&F settlement' });
}
};
export const getFnFById = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const fnf = await FnF.findByPk(id, {
include: [
{ model: Resignation, as: 'resignation' },
{ model: TerminationRequest, as: 'terminationRequest' },
{
model: Outlet, as: 'outlet',
include: [{ model: User, as: 'dealer' }]
},
{ model: FnFLineItem, as: 'lineItems' }
]
});
if (!fnf) return res.status(404).json({ success: false, message: 'F&F not found' });
res.json({ success: true, fnf });
} catch (error) {
console.error('Get F&F by ID error:', error);
res.status(500).json({ success: false, message: 'Error fetching F&F' });
}
};
export const addLineItem = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { itemType, description, department, amount } = req.body;
const lineItem = await FnFLineItem.create({
fnfId: id,
itemType,
description,
department,
amount,
addedBy: req.user?.id
});
res.json({ success: true, lineItem });
} catch (error) {
console.error('Add line item error:', error);
res.status(500).json({ success: false, message: 'Error adding line item' });
}
};
export const updateLineItem = async (req: AuthRequest, res: Response) => {
try {
const { itemId } = req.params;
const { description, department, amount } = req.body;
const lineItem = await FnFLineItem.findByPk(itemId);
if (!lineItem) return res.status(404).json({ success: false, message: 'Line item not found' });
await lineItem.update({ description, department, amount });
res.json({ success: true, lineItem });
} catch (error) {
console.error('Update line item error:', error);
res.status(500).json({ success: false, message: 'Error updating line item' });
}
};
export const deleteLineItem = async (req: AuthRequest, res: Response) => {
try {
const { itemId } = req.params;
const lineItem = await FnFLineItem.findByPk(itemId);
if (!lineItem) return res.status(404).json({ success: false, message: 'Line item not found' });
await lineItem.destroy();
res.json({ success: true, message: 'Line item deleted' });
} catch (error) {
console.error('Delete line item error:', error);
res.status(500).json({ success: false, message: 'Error deleting line item' });
}
};
export const calculateFnF = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const fnf = await FnF.findByPk(id, {
include: [{ model: FnFLineItem, as: 'lineItems' }]
});
if (!fnf) return res.status(404).json({ success: false, message: 'F&F not found' });
const lineItems = (fnf as any).lineItems || [];
let totalReceivables = 0;
let totalPayables = 0;
let totalDeductions = 0;
lineItems.forEach((item: any) => {
const amt = parseFloat(item.amount) || 0;
if (item.itemType === 'Receivable') totalReceivables += amt;
else if (item.itemType === 'Payable') totalPayables += amt;
else if (item.itemType === 'Deduction') totalDeductions += amt;
});
const netAmount = totalPayables - totalReceivables - totalDeductions;
await fnf.update({
totalReceivables,
totalPayables,
netAmount,
status: 'Calculated'
});
res.json({ success: true, fnf });
} catch (error) {
console.error('Calculate F&F error:', error);
res.status(500).json({ success: false, message: 'Error calculating F&F' });
}
};

View File

@ -11,7 +11,14 @@ router.use(authenticate as any);
// Finance user only routes
router.get('/onboarding', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.getOnboardingPayments);
router.get('/fnf', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.getFnFSettlements);
router.get('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_HEAD, ROLES.DD_LEAD]) as any, settlementController.getFnFById);
router.put('/payments/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.updatePayment);
router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.updateFnF);
router.post('/fnf/:id/calculate', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.calculateFnF);
// Line item management
router.post('/fnf/:id/line-items', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.addLineItem);
router.put('/fnf/line-items/:itemId', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.updateLineItem);
router.delete('/fnf/line-items/:itemId', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]) as any, settlementController.deleteLineItem);
export default router;

View File

@ -4,6 +4,7 @@ import logger from '../../common/utils/logger.js';
import { TERMINATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
import { Transaction } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js';
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
// Calculate progress percentage based on stage
const calculateProgress = (stage: string): number => {
@ -166,6 +167,49 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
remarks
}]
}, { transaction });
// If Terminated, create F&F record and fetch mock SAP dues
if (nextStage === TERMINATION_STAGES.TERMINATED) {
const dealer = await db.Dealer.findByPk(termination.dealerId);
const sapDues = await ExternalMocksService.mockGetFinancialDuesFromSap(dealer?.dealerCode || 'MOCK-001');
const fnf = await db.FnF.create({
terminationRequestId: termination.id,
dealerId: termination.dealerId,
status: 'Initiated',
totalReceivables: sapDues.data.outstandingInvoices,
totalPayables: sapDues.data.securityDeposit,
netAmount: sapDues.data.securityDeposit - sapDues.data.outstandingInvoices
}, { transaction });
// Create initial line items from SAP data
await db.FnFLineItem.bulkCreate([
{
fnfId: fnf.id,
itemType: 'Receivable',
description: 'Outstanding Invoices from SAP',
department: 'Finance',
amount: sapDues.data.outstandingInvoices,
addedBy: req.user.id
},
{
fnfId: fnf.id,
itemType: 'Payable',
description: 'Security Deposit from SAP',
department: 'Finance',
amount: sapDues.data.securityDeposit,
addedBy: req.user.id
}
], { transaction });
logger.info(`F&F record and mock line items created for termination: ${termination.id}`);
// Sync with Mock SAP
if (dealer) {
ExternalMocksService.mockSyncDealerStatusToSap(dealer.dealerCode, 'Inactive')
.catch(err => logger.error('Error syncing termination to SAP:', err));
}
}
}
await transaction.commit();