289 lines
13 KiB
TypeScript
289 lines
13 KiB
TypeScript
import { Response, NextFunction } from 'express';
|
|
import db from '../../database/models/index.js';
|
|
import logger from '../../common/utils/logger.js';
|
|
import { TERMINATION_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';
|
|
|
|
import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js';
|
|
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
|
import { ParticipantService } from '../../services/ParticipantService.js';
|
|
|
|
// Create termination request
|
|
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
const { dealerId, category, reason, proposedLwd, comments } = req.body;
|
|
|
|
const requestId = NomenclatureService.generateTerminationId();
|
|
const termination = await db.TerminationRequest.create({
|
|
requestId,
|
|
dealerId,
|
|
category,
|
|
reason,
|
|
proposedLwd,
|
|
comments,
|
|
initiatedBy: req.user.id,
|
|
currentStage: TERMINATION_STAGES.SUBMITTED,
|
|
status: 'Submitted',
|
|
progressPercentage: TerminationWorkflowService.calculateProgress(TERMINATION_STAGES.SUBMITTED),
|
|
timeline: [{
|
|
stage: 'Submitted',
|
|
timestamp: new Date(),
|
|
user: req.user.fullName,
|
|
action: 'Termination request initiated',
|
|
remarks: comments
|
|
}]
|
|
}, { transaction });
|
|
|
|
await db.AuditLog.create({
|
|
userId: req.user.id,
|
|
action: AUDIT_ACTIONS.CREATED,
|
|
entityType: 'termination',
|
|
entityId: termination.id
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
|
|
// Add as chat participants (Async)
|
|
ParticipantService.assignTerminationParticipants(termination.id)
|
|
.catch(err => logger.error('Error assigning participants to termination:', err));
|
|
|
|
res.status(201).json({ success: true, message: 'Termination request created', termination });
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error creating termination:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Get all termination requests
|
|
export const getTerminations = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
const { dealerId } = req.query;
|
|
const where: any = {};
|
|
|
|
if (dealerId) where.dealerId = dealerId;
|
|
if (req.user.roleCode === ROLES.DEALER) {
|
|
const dealer = await db.Dealer.findOne({ where: { userId: req.user.id } });
|
|
if (dealer) where.dealerId = dealer.id;
|
|
}
|
|
|
|
const terminations = await db.TerminationRequest.findAll({
|
|
where,
|
|
include: [
|
|
{
|
|
model: db.Dealer,
|
|
as: 'dealer',
|
|
include: [{ model: db.DealerCode, as: 'dealerCode' }]
|
|
}
|
|
],
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
res.json({ success: true, terminations });
|
|
} catch (error) {
|
|
logger.error('Error fetching terminations:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Get termination request by ID
|
|
export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const idStr = String(id);
|
|
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
|
|
const termination = await db.TerminationRequest.findOne({
|
|
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr },
|
|
include: [
|
|
{
|
|
model: db.Dealer,
|
|
as: 'dealer',
|
|
include: [
|
|
{
|
|
model: db.Application,
|
|
as: 'application',
|
|
include: [
|
|
{ model: db.District, as: 'district' },
|
|
{
|
|
model: db.LoiRequest,
|
|
as: 'loiRequests',
|
|
where: { status: 'approved' },
|
|
required: false
|
|
},
|
|
{
|
|
model: db.LoaRequest,
|
|
as: 'loaRequests',
|
|
where: { status: 'approved' },
|
|
required: false
|
|
}
|
|
]
|
|
},
|
|
{ model: db.DealerCode, as: 'dealerCode' }
|
|
]
|
|
},
|
|
{ model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'email'] },
|
|
{
|
|
model: db.TerminationDocument,
|
|
as: 'uploadedDocuments',
|
|
include: [{ model: db.User, as: 'uploader', attributes: ['fullName'] }]
|
|
},
|
|
{ model: db.FnF, as: 'fnfSettlement' },
|
|
{
|
|
model: db.RequestParticipant,
|
|
as: 'participants',
|
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'roleCode'] }]
|
|
}
|
|
]
|
|
});
|
|
|
|
if (!termination) {
|
|
return res.status(404).json({ success: false, message: 'Termination request not found' });
|
|
}
|
|
res.json({ success: true, termination });
|
|
} catch (error) {
|
|
logger.error('Error fetching termination:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Update termination status (Approve/Reject)
|
|
export const updateTerminationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
const { id } = req.params;
|
|
const { action, remarks } = req.body;
|
|
|
|
const termination = await db.TerminationRequest.findByPk(id);
|
|
if (!termination) {
|
|
await transaction.rollback();
|
|
return res.status(404).json({ success: false, message: 'Termination not found' });
|
|
}
|
|
|
|
if (action === 'reject') {
|
|
await TerminationWorkflowService.transitionTermination(termination, TERMINATION_STAGES.REJECTED, req.user.id, {
|
|
action: 'Rejected',
|
|
status: 'Rejected',
|
|
remarks
|
|
});
|
|
} else {
|
|
const stageFlow: Record<string, string> = {
|
|
[TERMINATION_STAGES.SUBMITTED]: TERMINATION_STAGES.RBM_REVIEW,
|
|
[TERMINATION_STAGES.RBM_REVIEW]: TERMINATION_STAGES.ZBH_REVIEW,
|
|
[TERMINATION_STAGES.ZBH_REVIEW]: TERMINATION_STAGES.DD_LEAD_REVIEW,
|
|
[TERMINATION_STAGES.DD_LEAD_REVIEW]: TERMINATION_STAGES.LEGAL_VERIFICATION,
|
|
[TERMINATION_STAGES.LEGAL_VERIFICATION]: TERMINATION_STAGES.NBH_EVALUATION,
|
|
[TERMINATION_STAGES.NBH_EVALUATION]: TERMINATION_STAGES.SCN_ISSUED,
|
|
[TERMINATION_STAGES.SCN_ISSUED]: TERMINATION_STAGES.PERSONAL_HEARING,
|
|
[TERMINATION_STAGES.PERSONAL_HEARING]: TERMINATION_STAGES.NBH_FINAL_APPROVAL,
|
|
[TERMINATION_STAGES.NBH_FINAL_APPROVAL]: TERMINATION_STAGES.CCO_APPROVAL,
|
|
[TERMINATION_STAGES.CCO_APPROVAL]: TERMINATION_STAGES.CEO_APPROVAL,
|
|
[TERMINATION_STAGES.CEO_APPROVAL]: TERMINATION_STAGES.LEGAL_LETTER,
|
|
[TERMINATION_STAGES.LEGAL_LETTER]: TERMINATION_STAGES.TERMINATED
|
|
};
|
|
|
|
const nextStage = stageFlow[termination.currentStage];
|
|
if (!nextStage) {
|
|
await transaction.rollback();
|
|
return res.status(400).json({ success: false, message: 'Cannot approve from current stage' });
|
|
}
|
|
|
|
await TerminationWorkflowService.transitionTermination(termination, nextStage, req.user.id, {
|
|
remarks,
|
|
status: nextStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : `${nextStage}`
|
|
});
|
|
|
|
// If Terminated, create F&F record and clearances
|
|
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 });
|
|
|
|
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 });
|
|
|
|
const { FNF_DEPARTMENTS } = await import('../../common/config/constants.js');
|
|
await db.FffClearance.bulkCreate(
|
|
FNF_DEPARTMENTS.map(dept => ({
|
|
fnfId: fnf.id,
|
|
department: dept,
|
|
status: 'Pending'
|
|
})),
|
|
{ transaction }
|
|
);
|
|
|
|
if (dealer) {
|
|
ExternalMocksService.mockSyncDealerStatusToSap(dealer.dealerCode, 'Inactive')
|
|
.catch(err => logger.error('Error syncing termination to SAP:', err));
|
|
}
|
|
}
|
|
}
|
|
|
|
await transaction.commit();
|
|
res.json({ success: true, message: 'Termination updated', termination });
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error updating termination:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// Submit SCN Response (Dealer Principal)
|
|
export const submitScnResponse = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
const { terminationRequestId, responseBody, documents } = req.body;
|
|
|
|
const termination = await db.TerminationRequest.findByPk(terminationRequestId);
|
|
if (!termination) throw new Error('Termination request not found');
|
|
|
|
const response = await TerminationWorkflowService.handleScnResponse(termination, { responseBody, documents }, req.user.id);
|
|
|
|
await transaction.commit();
|
|
res.status(201).json({ success: true, message: 'SCN Response submitted successfully', response });
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error submitting SCN response:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Record Personal Hearing Outcome
|
|
export const recordPersonalHearing = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
const { terminationRequestId, attendees, summary, recommendation, momDocumentId } = req.body;
|
|
|
|
const termination = await db.TerminationRequest.findByPk(terminationRequestId);
|
|
if (!termination) throw new Error('Termination request not found');
|
|
|
|
const hearing = await TerminationWorkflowService.handleHearingOutcome(termination, { attendees, summary, recommendation, momDocumentId }, req.user.id);
|
|
|
|
await transaction.commit();
|
|
res.status(201).json({ success: true, message: 'Hearing record saved', recommendation });
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error recording hearing:', error);
|
|
next(error);
|
|
}
|
|
};
|