Dealer_Onboarding_Backend/src/modules/termination/termination.controller.ts

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);
}
};