schema enhanced and made nececaasry model changes

This commit is contained in:
laxmanhalaki 2026-03-06 19:40:02 +05:30
parent 6b68364785
commit 92169b0fba
15 changed files with 608 additions and 133 deletions

View File

@ -241,58 +241,63 @@ erDiagram
%% DEALER ENTITY (POST-ONBOARDING)
%% ============================================
DEALERS {
uuid dealer_id PK
uuid application_id FK
string dealer_code UK
string dealer_name
string constitution_type
text registered_address
string gst_number
string pan_number
uuid id PK
uuid applicationId FK
uuid dealerCodeId FK
string legalName
string businessName
string constitutionType
text registeredAddress
string gstNumber
string panNumber
string status
date activation_date
date last_working_day
boolean portal_access_active
timestamp onboardedAt
timestamp created_at
timestamp updated_at
}
%% ============================================
%% DEALER APPLICATION
%% ONBOARDING APPLICATION (ONBOARDING PHASE)
%% ============================================
APPLICATIONS {
uuid application_id PK
string registration_number UK
string applicant_name
uuid id PK
string applicationId UK
uuid opportunityId FK
string applicantName
string email UK
string mobile_number
integer age
string country
string phone
string businessType
string preferredLocation
string city
string state
string district
string pincode
string interested_city
string company_name
string education_qualification
boolean owns_re_bike
boolean is_existing_dealer
text address
integer experienceYears
string investmentCapacity
integer age
string education
string companyName
string source
string existingDealer
string ownRoyalEnfield
string royalEnfieldModel
text description
string preferred_location
string application_status
string opportunity_status
boolean is_shortlisted
boolean dd_lead_shortlisted
uuid assigned_to FK
uuid zone_id FK
uuid region_id FK
uuid area_id FK
uuid assigned_dd_zm FK
uuid assigned_rbm FK
text address
string pincode
string locationType
string currentStage
string overallStatus
integer progressPercentage
boolean isShortlisted
boolean ddLeadShortlisted
decimal score
uuid assignedTo FK
uuid architectureAssignedTo FK
string architectureStatus
uuid submittedBy FK
uuid zoneId FK
uuid regionId FK
uuid areaId FK
json documents
json timeline
integer progress_percentage
timestamp submitted_at
timestamp created_at
timestamp updated_at
}
@ -660,7 +665,7 @@ erDiagram
%% OUTLET MANAGEMENT
%% ============================================
OUTLETS {
uuid outlet_id PK
uuid id PK
string code UK
string name
string type
@ -671,8 +676,8 @@ erDiagram
decimal latitude
decimal longitude
string status
date established_date
uuid dealer_id FK
date establishedDate
uuid dealerId FK
string region
string zone
timestamp created_at
@ -699,49 +704,62 @@ erDiagram
}
%% ============================================
%% DEALER SELF-SERVICE (SECTION 12)
%% DEALER SERVICE APPLICATIONS (POST-ONBOARDING)
%% Also known as Self-Service or Lifecycle Requests
%% ============================================
DEALER_RESIGNATIONS {
uuid resignation_id PK
uuid dealer_id FK
string outlet_code
date last_operational_date_sales
date last_operational_date_service
date proposed_lwd
string reason_type
text reason_description
RESIGNATIONS {
uuid id PK
string resignationId UK
uuid outletId FK
uuid dealerId FK
string resignationType
date lastOperationalDateSales
date lastOperationalDateServices
text reason
text additionalInfo
string currentStage
string status
boolean is_withdrawn
timestamp submitted_at
integer progressPercentage
timestamp submittedOn
json documents
json timeline
text rejectionReason
json departmentalClearances
timestamp created_at
timestamp updated_at
}
DEALER_RELOCATIONS {
uuid relocation_id PK
uuid dealer_id FK
string current_location_json
string proposed_location_json
decimal distance_km
string property_type
date expected_relocation_date
RELOCATION_REQUESTS {
uuid id PK
string requestId UK
uuid outletId FK
uuid dealerId FK
string relocationType
text newAddress
string newCity
string newState
text reason
string currentStage
string status
timestamp submitted_at
}
DEALER_CONSTITUTION_CHANGES {
uuid constitution_change_id PK
string request_id UK
uuid outlet_id FK
uuid dealer_id FK
string change_type
text description
string current_stage
string status
integer progress_percentage
integer progressPercentage
json documents
json timeline
timestamp created_at
timestamp updated_at
}
CONSTITUTIONAL_CHANGES {
uuid id PK
string requestId UK
uuid outletId FK
uuid dealerId FK
string changeType
text description
string currentStage
string status
integer progressPercentage
json documents
json timeline
timestamp submitted_at
timestamp created_at
timestamp updated_at
}
@ -883,17 +901,22 @@ erDiagram
}
%% ============================================
%% TERMINATION & F&F SETTLEMENT (SECTION 4.3 & 10)
%% TERMINATION (A TYPE OF SERVICE APPLICATION)
%% ============================================
TERMINATION_REQUESTS {
uuid termination_id PK
uuid dealer_id FK
uuid id PK
uuid dealerId FK
string category
text reason
date proposed_lwd
date proposedLwd
string status
uuid initiated_by FK
string currentStage
uuid initiatedBy FK
text comments
json timeline
json documents
timestamp created_at
timestamp updated_at
}
TERMINATION_APPROVALS {
@ -907,14 +930,20 @@ erDiagram
timestamp created_at
}
FNF_CASES {
uuid fnf_id PK
uuid dealer_id FK
uuid source_id FK
string source_type
date last_working_day
FNF_SETTLEMENTS {
uuid id PK
uuid resignationId FK
uuid terminationRequestId FK
uuid outletId FK
uuid dealerId FK
string status
timestamp initiated_at
decimal totalReceivables
decimal totalPayables
decimal netAmount
date settlementDate
json clearanceDocuments
timestamp created_at
timestamp updated_at
}
FNF_DEPARTMENT_CLEARANCES {
@ -1149,6 +1178,26 @@ erDiagram
timestamp access_revoked_at
}
REQUEST_PARTICIPANTS {
uuid id PK
uuid requestId FK
string requestType
uuid userId FK
string role
boolean isActive
timestamp created_at
timestamp updated_at
}
%% NOTE: Polymorphic Relationships
%% DOCUMENTS, WORK_NOTES, and REQUEST_PARTICIPANTS
%% use 'requestType' and 'requestId' to link with:
%% - APPLICATIONS (Onboarding)
%% - RESIGNATIONS
%% - RELOCATION_REQUESTS
%% - CONSTITUTIONAL_CHANGES
%% - TERMINATION_REQUESTS
%% ============================================
%% RELATIONSHIPS
%% ============================================
@ -1226,16 +1275,18 @@ erDiagram
APPLICATIONS ||--o{ DEALERS : "onboarded_as"
DEALERS ||--o{ USERS : "has_portal_users"
DEALERS ||--o{ DEALER_RESIGNATIONS : "initiates"
DEALERS ||--o{ DEALER_RELOCATIONS : "requests"
DEALERS ||--o{ DEALER_CONSTITUTION_CHANGES : "proposes"
DEALERS ||--o{ RESIGNATIONS : "initiates"
DEALERS ||--o{ RELOCATION_REQUESTS : "requests"
DEALERS ||--o{ CONSTITUTIONAL_CHANGES : "proposes"
DEALERS ||--o{ TERMINATION_REQUESTS : "terminated_by"
TERMINATION_REQUESTS ||--o{ TERMINATION_APPROVALS : "requires"
DEALERS ||--o{ FNF_CASES : "settled_in"
FNF_CASES ||--o{ FNF_DEPARTMENT_CLEARANCES : "requires_NOC_from"
FNF_CASES ||--o{ FNF_SETTLEMENT_SUMMARIES : "consolidated_in"
DEALERS ||--o{ FNF_SETTLEMENTS : "settled_in"
RESIGNATIONS ||--o{ FNF_SETTLEMENTS : "triggers"
TERMINATION_REQUESTS ||--o{ FNF_SETTLEMENTS : "triggers"
FNF_SETTLEMENTS ||--o{ FNF_DEPARTMENT_CLEARANCES : "requires_NOC_from"
FNF_SETTLEMENTS ||--o{ FNF_SETTLEMENT_SUMMARIES : "consolidated_in"
DEALERS ||--o{ DEALER_PORTAL_CONFIG : "governed_by"
@ -1271,21 +1322,29 @@ erDiagram
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_REMINDERS : "defines"
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_ESCALATIONS : "defines"
FNF_CASES ||--o{ FNF_LINE_ITEMS : "has"
FNF_SETTLEMENTS ||--o{ FNF_LINE_ITEMS : "has"
FNF_DEPARTMENT_CLEARANCES ||--o{ FNF_LINE_ITEMS : "details"
USERS ||--o{ FNF_LINE_ITEMS : "added"
USERS ||--o{ APPLICATIONS : "currently_assigned"
USERS ||--o{ OUTLETS : "has_outlets"
OUTLETS ||--o{ DEALER_CONSTITUTION_CHANGES : "requests_change"
OUTLETS ||--o{ CONSTITUTIONAL_CHANGES : "requests_change"
OUTLETS ||--o{ RELOCATION_REQUESTS : "requests_relocation"
OUTLETS ||--o{ RESIGNATIONS : "initiates_resignation"
APPLICATIONS ||--o{ FINANCE_PAYMENTS : "has_payments"
USERS ||--o{ FINANCE_PAYMENTS : "verifies_payments"
DEALER_RESIGNATIONS ||--o{ EXIT_FEEDBACK : "has_feedback"
RESIGNATIONS ||--o{ EXIT_FEEDBACK : "has_feedback"
TERMINATION_REQUESTS ||--o{ EXIT_FEEDBACK : "has_feedback"
USERS ||--o{ EXIT_FEEDBACK : "submitted_by"
SLA_TRACKING ||--o{ SLA_BREACHES : "has_breaches"
APPLICATIONS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
RESIGNATIONS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
TERMINATION_REQUESTS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
RELOCATION_REQUESTS ||--o{ REQUEST_PARTICIPANTS : "has_participants"
CONSTITUTIONAL_CHANGES ||--o{ REQUEST_PARTICIPANTS : "has_participants"

View File

@ -34,6 +34,7 @@ export const APPLICATION_STAGES = {
DD_HEAD: 'DD Head',
NBH: 'NBH',
LEGAL: 'Legal',
ARCHITECTURE: 'Architecture',
FINANCE: 'Finance',
LEVEL_1_APPROVED: 'Level 1 Approved',
LEVEL_2_APPROVED: 'Level 2 Approved',
@ -84,6 +85,24 @@ export const APPLICATION_STATUS = {
DISQUALIFIED: 'Disqualified'
} as const;
// Termination Stages
export const TERMINATION_STAGES = {
SUBMITTED: 'Submitted',
RBM_REVIEW: 'RBM Review',
ZBH_REVIEW: 'ZBH Review',
DD_LEAD_REVIEW: 'DD Lead Review',
LEGAL_VERIFICATION: 'Legal Verification',
NBH_EVALUATION: 'NBH Evaluation',
SCN_ISSUED: 'Show Cause Notice',
PERSONAL_HEARING: 'Personal Hearing',
NBH_FINAL_APPROVAL: 'NBH Final Approval',
CCO_APPROVAL: 'CCO Approval',
CEO_APPROVAL: 'CEO Final Approval',
LEGAL_LETTER: 'Legal - Termination Letter',
TERMINATED: 'Terminated',
REJECTED: 'Rejected'
} as const;
// Resignation Stages
export const RESIGNATION_STAGES = {
ASM: 'ASM',
@ -92,6 +111,9 @@ export const RESIGNATION_STAGES = {
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL: 'Legal',
SPARES_CLEARANCE: 'Spares Clearance',
SERVICE_CLEARANCE: 'Service Clearance',
ACCOUNTS_CLEARANCE: 'Accounts Clearance',
FINANCE: 'Finance',
FNF_INITIATED: 'F&F Initiated',
COMPLETED: 'Completed',

View File

@ -31,6 +31,8 @@ export interface ApplicationAttributes {
isShortlisted: boolean;
ddLeadShortlisted: boolean;
assignedTo: string | null;
architectureAssignedTo: string | null;
architectureStatus: string | null;
submittedBy: string | null;
zoneId: string | null;
regionId: string | null;
@ -176,6 +178,19 @@ export default (sequelize: Sequelize) => {
key: 'id'
}
},
architectureAssignedTo: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
architectureStatus: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: 'Pending'
},
submittedBy: {
type: DataTypes.UUID,
allowNull: true,
@ -231,6 +246,7 @@ export default (sequelize: Sequelize) => {
(Application as any).associate = (models: any) => {
Application.belongsTo(models.User, { foreignKey: 'submittedBy', as: 'submitter' });
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' });
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
Application.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
Application.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });

View File

@ -18,6 +18,12 @@ export interface ResignationAttributes {
documents: any[];
timeline: any[];
rejectionReason: string | null;
departmentalClearances: {
spares: boolean;
service: boolean;
accounts: boolean;
logistics: boolean;
} | null;
}
export interface ResignationInstance extends Model<ResignationAttributes>, ResignationAttributes { }
@ -97,6 +103,15 @@ export default (sequelize: Sequelize) => {
rejectionReason: {
type: DataTypes.TEXT,
allowNull: true
},
departmentalClearances: {
type: DataTypes.JSON,
defaultValue: {
spares: false,
service: false,
accounts: false,
logistics: false
}
}
}, {
tableName: 'resignations',

View File

@ -7,8 +7,11 @@ export interface TerminationRequestAttributes {
reason: string;
proposedLwd: Date;
status: string;
currentStage: string;
initiatedBy: string;
comments: string | null;
timeline: any[];
documents: any[];
}
export interface TerminationRequestInstance extends Model<TerminationRequestAttributes>, TerminationRequestAttributes { }
@ -42,7 +45,11 @@ export default (sequelize: Sequelize) => {
},
status: {
type: DataTypes.STRING,
defaultValue: 'pending'
defaultValue: 'Initiated'
},
currentStage: {
type: DataTypes.STRING,
defaultValue: 'INITIATED'
},
initiatedBy: {
type: DataTypes.UUID,
@ -55,6 +62,14 @@ export default (sequelize: Sequelize) => {
comments: {
type: DataTypes.TEXT,
allowNull: true
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
},
documents: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'termination_requests',

View File

@ -43,12 +43,17 @@
</div>
<div class="content">
<p>Dear {{applicantName}},</p>
<p>Thank you for showing interest in becoming a Royal Enfield dealer.</p>
<p>We have reviewed our current network plan for <strong>{{location}}</strong>, and currently, there are no
open opportunities available in this area.</p>
<p>We have saved your details in our database and will contact you should an opportunity arise in the
future.</p>
<p>We appreciate your enthusiasm for the brand.</p>
<p>Thank you for your interest in Royal Enfield and for your application to represent our brand as an
authorized dealer.</p>
<p>We have carefully reviewed your expression of interest in relation to our current network expansion
strategy for <strong>{{location}}</strong>. At this juncture, we do not have any immediate vacancies or
planned dealership opportunities available in this specific territory.</p>
<p>However, we have successfully retained your profile in our prospective partners database. Rest assured,
our team will proactively reach out to you should a suitable opportunity materialize in this region or
if our strategic requirements in your preferred location evolve further.</p>
<p>We appreciate the time you took to share your details and your continued enthusiasm for Royal Enfield.
</p>
<p>Best regards,<br>Dealer Development Team<br>Royal Enfield</p>
</div>
<div class="footer">
<p>&copy; {{year}} Royal Enfield. All rights reserved.</p>

View File

@ -425,3 +425,71 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
res.status(500).json({ success: false, message: 'Error processing shortlist' });
}
};
export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { userId, remarks } = req.body;
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
await application.update({
architectureAssignedTo: userId,
architectureStatus: 'Assigned',
updatedAt: new Date()
});
// Add as participant
await db.RequestParticipant.findOrCreate({
where: {
requestId: application.id,
requestType: 'application',
userId,
participantType: 'architecture'
},
defaults: { joinedMethod: 'auto' }
});
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'application',
entityId: application.id,
newData: { architectureAssignedTo: userId, remarks }
});
res.json({ success: true, message: 'Architecture team assigned successfully' });
} catch (error) {
console.error('Assign architecture team error:', error);
res.status(500).json({ success: false, message: 'Error assigning architecture team' });
}
};
export const updateArchitectureStatus = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { status, remarks } = req.body;
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
await application.update({
architectureStatus: status,
updatedAt: new Date()
});
await AuditLog.create({
userId: req.user?.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'application',
entityId: application.id,
newData: { architectureStatus: status, remarks }
});
res.json({ success: true, message: 'Architecture status updated successfully' });
} catch (error) {
console.error('Update architecture status error:', error);
res.status(500).json({ success: false, message: 'Error updating architecture status' });
}
};

View File

@ -1,24 +1,32 @@
import express from 'express';
const router = express.Router();
import * as onboardingController from './onboarding.controller.js';
import {
submitApplication, getApplications, getApplicationById, updateApplicationStatus,
uploadDocuments, getApplicationDocuments, bulkShortlist,
assignArchitectureTeam, updateArchitectureStatus
} from './onboarding.controller.js';
import { authenticate } from '../../common/middleware/auth.js';
import { uploadSingle } from '../../common/middleware/upload.js';
// All routes require authentication (or public for submission? Keeping auth for now)
// Public route for application submission
router.post('/apply', onboardingController.submitApplication);
router.post('/apply', submitApplication);
// All subsequent routes require authentication
router.use(authenticate as any);
router.get('/applications', onboardingController.getApplications);
router.post('/applications/shortlist', onboardingController.bulkShortlist);
router.get('/applications/:id', onboardingController.getApplicationById);
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
router.post('/applications/:id/documents', uploadSingle, onboardingController.uploadDocuments);
router.get('/applications/:id/documents', onboardingController.getApplicationDocuments);
router.get('/applications', getApplications);
router.post('/applications/shortlist', bulkShortlist); // Existing route, updated to named import
router.get('/applications/:id', getApplicationById);
router.put('/applications/:id/status', updateApplicationStatus);
router.post('/applications/:id/documents', uploadSingle, uploadDocuments);
router.get('/applications/:id/documents', getApplicationDocuments); // Existing route, updated to named import
// Architecture-related routes
router.post('/applications/:id/assign-architecture', assignArchitectureTeam);
router.put('/applications/:id/architecture-status', updateArchitectureStatus);
// Questionnaire Routes
router.get('/questionnaires', (req, res, next) => {

View File

@ -24,12 +24,12 @@ export const submitRequest = async (req: AuthRequest, res: Response) => {
description: reason,
currentStage: 'DD_ADMIN_REVIEW' as any,
status: 'Pending',
progressPercentage: 0,
progressPercentage: 20,
documents: [],
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
user: req.user.fullName,
action: 'Request submitted'
}]
});
@ -65,7 +65,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
{
model: User,
as: 'dealer',
attributes: ['name']
attributes: ['fullName']
}
],
order: [['createdAt', 'DESC']]
@ -97,7 +97,7 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
{
model: User,
as: 'dealer',
attributes: ['name', 'email']
attributes: ['fullName', 'email']
},
{
model: Worknote,
@ -137,16 +137,36 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
return res.status(404).json({ success: false, message: 'Request not found' });
}
const stageFlow: Record<string, string> = {
'DD_ADMIN_REVIEW': 'LEGAL_REVIEW',
'LEGAL_REVIEW': 'NBH_APPROVAL',
'NBH_APPROVAL': 'FINANCE_CLEARANCE',
'FINANCE_CLEARANCE': 'COMPLETED'
};
const currentStage = request.currentStage as string;
let nextStage = currentStage;
let finalStatus = action;
if (action === 'Approve') {
nextStage = stageFlow[currentStage] || currentStage;
finalStatus = nextStage === 'COMPLETED' ? 'Completed' : `Pending ${nextStage.replace('_', ' ')}`;
} else if (action === 'Reject') {
nextStage = 'REJECTED';
finalStatus = 'Rejected';
}
const timeline = [...request.timeline, {
stage: 'Review',
stage: currentStage,
timestamp: new Date(),
user: req.user.name,
user: req.user.fullName,
action,
remarks: comments
}];
await request.update({
status: action,
status: finalStatus,
currentStage: nextStage as any,
timeline,
updatedAt: new Date()
});

View File

@ -1,7 +1,7 @@
import { Response } from 'express';
import db from '../../database/models/index.js';
const { RelocationRequest, Outlet, User, Worknote } = db;
import { AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js';
import { Op, Transaction } from 'sequelize';
import { v4 as uuidv4 } from 'uuid';
import { AuthRequest } from '../../types/express.types.js';
@ -27,14 +27,14 @@ export const submitRequest = async (req: AuthRequest, res: Response) => {
newCity: proposedCity,
newState: proposedState,
reason,
currentStage: 'DD_ADMIN_REVIEW' as any,
currentStage: RELOCATION_STAGES.DD_ADMIN_REVIEW as any,
status: 'Pending',
progressPercentage: 0,
progressPercentage: 20,
documents: [],
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
user: req.user.fullName,
action: 'Request submitted'
}]
});
@ -70,7 +70,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
{
model: User,
as: 'dealer',
attributes: ['name']
attributes: ['fullName']
}
],
order: [['createdAt', 'DESC']]
@ -102,7 +102,7 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
{
model: User,
as: 'dealer',
attributes: ['name', 'email']
attributes: ['fullName', 'email']
},
{
model: Worknote,
@ -147,10 +147,19 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
let newStatus = request.status;
let newCurrentStage = request.currentStage;
const stageFlow: Record<string, string> = {
[RELOCATION_STAGES.DD_ADMIN_REVIEW]: RELOCATION_STAGES.RBM_REVIEW,
[RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.NBH_APPROVAL,
[RELOCATION_STAGES.NBH_APPROVAL]: RELOCATION_STAGES.LEGAL_CLEARANCE,
[RELOCATION_STAGES.LEGAL_CLEARANCE]: RELOCATION_STAGES.COMPLETED
};
if (action === 'Approved') {
newStatus = 'Approved';
newCurrentStage = stageFlow[request.currentStage] || request.currentStage;
newStatus = newCurrentStage === RELOCATION_STAGES.COMPLETED ? 'Completed' : `Pending ${newCurrentStage.replace('_', ' ')}`;
} else if (action === 'Rejected') {
newStatus = 'Rejected';
newCurrentStage = RELOCATION_STAGES.REJECTED;
}
// Create a worknote entry

View File

@ -18,8 +18,11 @@ const calculateProgress = (stage: string): number => {
[RESIGNATION_STAGES.RBM]: 30,
[RESIGNATION_STAGES.ZBH]: 45,
[RESIGNATION_STAGES.NBH]: 60,
[RESIGNATION_STAGES.DD_ADMIN]: 70,
[RESIGNATION_STAGES.LEGAL]: 80,
[RESIGNATION_STAGES.DD_ADMIN]: 65,
[RESIGNATION_STAGES.LEGAL]: 70,
[RESIGNATION_STAGES.SPARES_CLEARANCE]: 75,
[RESIGNATION_STAGES.SERVICE_CLEARANCE]: 80,
[RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: 85,
[RESIGNATION_STAGES.FINANCE]: 90,
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
[RESIGNATION_STAGES.COMPLETED]: 100,
@ -88,7 +91,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
user: req.user.fullName,
action: 'Resignation request submitted'
}]
}, { transaction });
@ -260,7 +263,10 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.FINANCE,
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.SPARES_CLEARANCE,
[RESIGNATION_STAGES.SPARES_CLEARANCE]: RESIGNATION_STAGES.SERVICE_CLEARANCE,
[RESIGNATION_STAGES.SERVICE_CLEARANCE]: RESIGNATION_STAGES.ACCOUNTS_CLEARANCE,
[RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: RESIGNATION_STAGES.FINANCE,
[RESIGNATION_STAGES.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED,
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED
};
@ -279,7 +285,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
const timeline = [...resignation.timeline, {
stage: nextStage,
timestamp: new Date(),
user: req.user.name,
user: req.user.fullName,
action: 'Approved',
remarks
}];
@ -357,7 +363,7 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
const timeline = [...resignation.timeline, {
stage: 'Rejected',
timestamp: new Date(),
user: req.user.name,
user: req.user.fullName,
action: 'Rejected',
reason
}];
@ -399,3 +405,39 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
next(error);
}
};
// Update departmental clearance
export const updateClearance = 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 { department, cleared, remarks } = req.body;
const resignation = await db.Resignation.findByPk(id);
if (!resignation) {
await transaction.rollback();
return res.status(404).json({ success: false, message: 'Resignation not found' });
}
const clearances = { ...resignation.departmentalClearances, [department]: cleared };
await resignation.update({
departmentalClearances: clearances,
timeline: [...resignation.timeline, {
stage: resignation.currentStage,
timestamp: new Date(),
user: req.user.fullName,
action: cleared ? `Cleared ${department}` : `Revoked ${department} clearance`,
remarks
}]
}, { transaction });
await transaction.commit();
res.json({ success: true, message: `Clearance updated for ${department}`, resignation });
} catch (error) {
if (transaction) await transaction.rollback();
logger.error('Error updating clearance:', error);
next(error);
}
};

View File

@ -9,5 +9,6 @@ router.get('/', authenticate as any, resignationController.getResignations);
router.get('/:id', authenticate as any, resignationController.getResignationById);
router.put('/:id/approve', authenticate as any, resignationController.approveResignation);
router.put('/:id/reject', authenticate as any, resignationController.rejectResignation);
router.put('/:id/clearance', authenticate as any, resignationController.updateClearance);
export default router;

View File

@ -0,0 +1,178 @@
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 { Transaction } from 'sequelize';
import { AuthRequest } from '../../types/express.types.js';
// Calculate progress percentage based on stage
const calculateProgress = (stage: string): number => {
const stageProgress: Record<string, number> = {
[TERMINATION_STAGES.SUBMITTED]: 10,
[TERMINATION_STAGES.RBM_REVIEW]: 20,
[TERMINATION_STAGES.ZBH_REVIEW]: 30,
[TERMINATION_STAGES.DD_LEAD_REVIEW]: 40,
[TERMINATION_STAGES.LEGAL_VERIFICATION]: 50,
[TERMINATION_STAGES.NBH_EVALUATION]: 60,
[TERMINATION_STAGES.SCN_ISSUED]: 70,
[TERMINATION_STAGES.PERSONAL_HEARING]: 75,
[TERMINATION_STAGES.NBH_FINAL_APPROVAL]: 80,
[TERMINATION_STAGES.CCO_APPROVAL]: 85,
[TERMINATION_STAGES.CEO_APPROVAL]: 90,
[TERMINATION_STAGES.LEGAL_LETTER]: 95,
[TERMINATION_STAGES.TERMINATED]: 100,
[TERMINATION_STAGES.REJECTED]: 0
};
return stageProgress[stage] || 0;
};
// 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;
// Restriction: Only ASM or RBM can initiate (as per user request: "ASM or RBM can initiate")
// Note: Check existing roles in constants. ROLES.RBM exists. ASM might be DD or similar.
// For now, I'll allow ASM (mapped to DD/Initiator) and RBM.
const termination = await db.TerminationRequest.create({
dealerId,
category,
reason,
proposedLwd,
comments,
initiatedBy: req.user.id,
currentStage: TERMINATION_STAGES.SUBMITTED,
status: '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();
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 terminations
export const getTerminations = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const terminations = await db.TerminationRequest.findAll({
include: [
{ model: db.Dealer, as: 'dealer' },
{ model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'roleCode'] }
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, terminations });
} catch (error) {
logger.error('Error fetching terminations:', error);
next(error);
}
};
// Get termination by ID
export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const termination = await db.TerminationRequest.findByPk(id, {
include: [
{ model: db.Dealer, as: 'dealer' },
{ model: db.User, as: 'initiator', attributes: ['id', 'fullName', 'roleCode'] }
]
});
if (!termination) return res.status(404).json({ success: false, message: 'Termination 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; // action: 'approve' | 'reject' | 'sendback'
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 termination.update({
currentStage: TERMINATION_STAGES.REJECTED,
status: 'Rejected',
timeline: [...termination.timeline, {
stage: 'Rejected',
timestamp: new Date(),
user: req.user.fullName,
action: 'Rejected',
remarks
}]
}, { transaction });
} else {
// Approval flow
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 termination.update({
currentStage: nextStage,
status: nextStage === TERMINATION_STAGES.TERMINATED ? 'Terminated' : `${nextStage}`,
timeline: [...termination.timeline, {
stage: nextStage,
timestamp: new Date(),
user: req.user.fullName,
action: 'Approved/Moved',
remarks
}]
}, { transaction });
}
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);
}
};

View File

@ -0,0 +1,15 @@
import express from 'express';
const router = express.Router();
import {
createTermination, getTerminations, getTerminationById, updateTerminationStatus
} from './termination.controller.js';
import { authenticate } from '../../common/middleware/auth.js';
router.use(authenticate as any);
router.post('/', createTermination);
router.get('/', getTerminations);
router.get('/:id', getTerminationById);
router.put('/:id/status', updateTerminationStatus);
export default router;

View File

@ -36,6 +36,7 @@ import communicationRoutes from './modules/communication/communication.routes.js
import auditRoutes from './modules/audit/audit.routes.js';
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
import prospectiveLoginRoutes from './modules/prospective-login/prospective-login.routes.js';
import terminationRoutes from './modules/termination/termination.routes.js';
// Import common middleware & utils
import errorHandler from './common/middleware/errorHandler.js';
@ -125,6 +126,7 @@ app.use('/api/communication', communicationRoutes);
app.use('/api/audit', auditRoutes);
app.use('/api/questionnaire', questionnaireRoutes);
app.use('/api/prospective-login', prospectiveLoginRoutes);
app.use('/api/termination', terminationRoutes);
// Backward Compatibility Aliases
app.use('/api/applications', onboardingRoutes);