master page modified and trying to cover all models necessary flow
This commit is contained in:
parent
92169b0fba
commit
c43f86253b
@ -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',
|
||||
|
||||
61
src/common/middleware/stageRoleCheck.ts
Normal file
61
src/common/middleware/stageRoleCheck.ts
Normal 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' });
|
||||
}
|
||||
};
|
||||
};
|
||||
115
src/common/utils/externalMocks.service.ts
Normal file
115
src/common/utils/externalMocks.service.ts
Normal 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;
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ---
|
||||
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user