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) %% DEALER ENTITY (POST-ONBOARDING)
%% ============================================ %% ============================================
DEALERS { DEALERS {
uuid dealer_id PK uuid id PK
uuid application_id FK uuid applicationId FK
string dealer_code UK uuid dealerCodeId FK
string dealer_name string legalName
string constitution_type string businessName
text registered_address string constitutionType
string gst_number text registeredAddress
string pan_number string gstNumber
string panNumber
string status string status
date activation_date timestamp onboardedAt
date last_working_day
boolean portal_access_active
timestamp created_at timestamp created_at
timestamp updated_at timestamp updated_at
} }
%% ============================================ %% ============================================
%% DEALER APPLICATION %% ONBOARDING APPLICATION (ONBOARDING PHASE)
%% ============================================ %% ============================================
APPLICATIONS { APPLICATIONS {
uuid application_id PK uuid id PK
string registration_number UK string applicationId UK
string applicant_name uuid opportunityId FK
string applicantName
string email UK string email UK
string mobile_number string phone
integer age string businessType
string country string preferredLocation
string city
string state string state
string district integer experienceYears
string pincode string investmentCapacity
string interested_city integer age
string company_name string education
string education_qualification string companyName
boolean owns_re_bike string source
boolean is_existing_dealer string existingDealer
text address string ownRoyalEnfield
string royalEnfieldModel
text description text description
string preferred_location text address
string application_status string pincode
string opportunity_status string locationType
boolean is_shortlisted string currentStage
boolean dd_lead_shortlisted string overallStatus
uuid assigned_to FK integer progressPercentage
uuid zone_id FK boolean isShortlisted
uuid region_id FK boolean ddLeadShortlisted
uuid area_id FK decimal score
uuid assigned_dd_zm FK uuid assignedTo FK
uuid assigned_rbm FK uuid architectureAssignedTo FK
string architectureStatus
uuid submittedBy FK
uuid zoneId FK
uuid regionId FK
uuid areaId FK
json documents json documents
json timeline json timeline
integer progress_percentage
timestamp submitted_at
timestamp created_at timestamp created_at
timestamp updated_at timestamp updated_at
} }
@ -660,7 +665,7 @@ erDiagram
%% OUTLET MANAGEMENT %% OUTLET MANAGEMENT
%% ============================================ %% ============================================
OUTLETS { OUTLETS {
uuid outlet_id PK uuid id PK
string code UK string code UK
string name string name
string type string type
@ -671,8 +676,8 @@ erDiagram
decimal latitude decimal latitude
decimal longitude decimal longitude
string status string status
date established_date date establishedDate
uuid dealer_id FK uuid dealerId FK
string region string region
string zone string zone
timestamp created_at 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 { RESIGNATIONS {
uuid resignation_id PK uuid id PK
uuid dealer_id FK string resignationId UK
string outlet_code uuid outletId FK
date last_operational_date_sales uuid dealerId FK
date last_operational_date_service string resignationType
date proposed_lwd date lastOperationalDateSales
string reason_type date lastOperationalDateServices
text reason_description text reason
text additionalInfo
string currentStage
string status string status
boolean is_withdrawn integer progressPercentage
timestamp submitted_at timestamp submittedOn
json documents
json timeline
text rejectionReason
json departmentalClearances
timestamp created_at
timestamp updated_at timestamp updated_at
} }
DEALER_RELOCATIONS { RELOCATION_REQUESTS {
uuid relocation_id PK uuid id PK
uuid dealer_id FK string requestId UK
string current_location_json uuid outletId FK
string proposed_location_json uuid dealerId FK
decimal distance_km string relocationType
string property_type text newAddress
date expected_relocation_date string newCity
string newState
text reason text reason
string currentStage
string status string status
timestamp submitted_at integer progressPercentage
} json documents
json timeline
DEALER_CONSTITUTION_CHANGES { timestamp created_at
uuid constitution_change_id PK timestamp updated_at
string request_id UK }
uuid outlet_id FK
uuid dealer_id FK CONSTITUTIONAL_CHANGES {
string change_type uuid id PK
text description string requestId UK
string current_stage uuid outletId FK
string status uuid dealerId FK
integer progress_percentage string changeType
text description
string currentStage
string status
integer progressPercentage
json documents json documents
json timeline json timeline
timestamp submitted_at
timestamp created_at timestamp created_at
timestamp updated_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 { TERMINATION_REQUESTS {
uuid termination_id PK uuid id PK
uuid dealer_id FK uuid dealerId FK
string category string category
text reason text reason
date proposed_lwd date proposedLwd
string status string status
uuid initiated_by FK string currentStage
uuid initiatedBy FK
text comments
json timeline
json documents
timestamp created_at timestamp created_at
timestamp updated_at
} }
TERMINATION_APPROVALS { TERMINATION_APPROVALS {
@ -907,14 +930,20 @@ erDiagram
timestamp created_at timestamp created_at
} }
FNF_CASES { FNF_SETTLEMENTS {
uuid fnf_id PK uuid id PK
uuid dealer_id FK uuid resignationId FK
uuid source_id FK uuid terminationRequestId FK
string source_type uuid outletId FK
date last_working_day uuid dealerId FK
string status string status
timestamp initiated_at decimal totalReceivables
decimal totalPayables
decimal netAmount
date settlementDate
json clearanceDocuments
timestamp created_at
timestamp updated_at
} }
FNF_DEPARTMENT_CLEARANCES { FNF_DEPARTMENT_CLEARANCES {
@ -1149,6 +1178,26 @@ erDiagram
timestamp access_revoked_at 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 %% RELATIONSHIPS
%% ============================================ %% ============================================
@ -1226,16 +1275,18 @@ erDiagram
APPLICATIONS ||--o{ DEALERS : "onboarded_as" APPLICATIONS ||--o{ DEALERS : "onboarded_as"
DEALERS ||--o{ USERS : "has_portal_users" DEALERS ||--o{ USERS : "has_portal_users"
DEALERS ||--o{ DEALER_RESIGNATIONS : "initiates" DEALERS ||--o{ RESIGNATIONS : "initiates"
DEALERS ||--o{ DEALER_RELOCATIONS : "requests" DEALERS ||--o{ RELOCATION_REQUESTS : "requests"
DEALERS ||--o{ DEALER_CONSTITUTION_CHANGES : "proposes" DEALERS ||--o{ CONSTITUTIONAL_CHANGES : "proposes"
DEALERS ||--o{ TERMINATION_REQUESTS : "terminated_by" DEALERS ||--o{ TERMINATION_REQUESTS : "terminated_by"
TERMINATION_REQUESTS ||--o{ TERMINATION_APPROVALS : "requires" TERMINATION_REQUESTS ||--o{ TERMINATION_APPROVALS : "requires"
DEALERS ||--o{ FNF_CASES : "settled_in" DEALERS ||--o{ FNF_SETTLEMENTS : "settled_in"
FNF_CASES ||--o{ FNF_DEPARTMENT_CLEARANCES : "requires_NOC_from" RESIGNATIONS ||--o{ FNF_SETTLEMENTS : "triggers"
FNF_CASES ||--o{ FNF_SETTLEMENT_SUMMARIES : "consolidated_in" 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" DEALERS ||--o{ DEALER_PORTAL_CONFIG : "governed_by"
@ -1271,21 +1322,29 @@ erDiagram
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_REMINDERS : "defines" SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_REMINDERS : "defines"
SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_ESCALATIONS : "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" FNF_DEPARTMENT_CLEARANCES ||--o{ FNF_LINE_ITEMS : "details"
USERS ||--o{ FNF_LINE_ITEMS : "added" USERS ||--o{ FNF_LINE_ITEMS : "added"
USERS ||--o{ APPLICATIONS : "currently_assigned" USERS ||--o{ APPLICATIONS : "currently_assigned"
USERS ||--o{ OUTLETS : "has_outlets" 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" APPLICATIONS ||--o{ FINANCE_PAYMENTS : "has_payments"
USERS ||--o{ FINANCE_PAYMENTS : "verifies_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" TERMINATION_REQUESTS ||--o{ EXIT_FEEDBACK : "has_feedback"
USERS ||--o{ EXIT_FEEDBACK : "submitted_by" USERS ||--o{ EXIT_FEEDBACK : "submitted_by"
SLA_TRACKING ||--o{ SLA_BREACHES : "has_breaches" 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', DD_HEAD: 'DD Head',
NBH: 'NBH', NBH: 'NBH',
LEGAL: 'Legal', LEGAL: 'Legal',
ARCHITECTURE: 'Architecture',
FINANCE: 'Finance', FINANCE: 'Finance',
LEVEL_1_APPROVED: 'Level 1 Approved', LEVEL_1_APPROVED: 'Level 1 Approved',
LEVEL_2_APPROVED: 'Level 2 Approved', LEVEL_2_APPROVED: 'Level 2 Approved',
@ -84,6 +85,24 @@ export const APPLICATION_STATUS = {
DISQUALIFIED: 'Disqualified' DISQUALIFIED: 'Disqualified'
} as const; } 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 // Resignation Stages
export const RESIGNATION_STAGES = { export const RESIGNATION_STAGES = {
ASM: 'ASM', ASM: 'ASM',
@ -92,6 +111,9 @@ export const RESIGNATION_STAGES = {
NBH: 'NBH', NBH: 'NBH',
DD_ADMIN: 'DD Admin', DD_ADMIN: 'DD Admin',
LEGAL: 'Legal', LEGAL: 'Legal',
SPARES_CLEARANCE: 'Spares Clearance',
SERVICE_CLEARANCE: 'Service Clearance',
ACCOUNTS_CLEARANCE: 'Accounts Clearance',
FINANCE: 'Finance', FINANCE: 'Finance',
FNF_INITIATED: 'F&F Initiated', FNF_INITIATED: 'F&F Initiated',
COMPLETED: 'Completed', COMPLETED: 'Completed',

View File

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

View File

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

View File

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

View File

@ -43,12 +43,17 @@
</div> </div>
<div class="content"> <div class="content">
<p>Dear {{applicantName}},</p> <p>Dear {{applicantName}},</p>
<p>Thank you for showing interest in becoming a Royal Enfield dealer.</p> <p>Thank you for your interest in Royal Enfield and for your application to represent our brand as an
<p>We have reviewed our current network plan for <strong>{{location}}</strong>, and currently, there are no authorized dealer.</p>
open opportunities available in this area.</p> <p>We have carefully reviewed your expression of interest in relation to our current network expansion
<p>We have saved your details in our database and will contact you should an opportunity arise in the strategy for <strong>{{location}}</strong>. At this juncture, we do not have any immediate vacancies or
future.</p> planned dealership opportunities available in this specific territory.</p>
<p>We appreciate your enthusiasm for the brand.</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>
<div class="footer"> <div class="footer">
<p>&copy; {{year}} Royal Enfield. All rights reserved.</p> <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' }); 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'; import express from 'express';
const router = express.Router(); 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 { authenticate } from '../../common/middleware/auth.js';
import { uploadSingle } from '../../common/middleware/upload.js'; import { uploadSingle } from '../../common/middleware/upload.js';
// All routes require authentication (or public for submission? Keeping auth for now) // All routes require authentication (or public for submission? Keeping auth for now)
// Public route for application submission // Public route for application submission
router.post('/apply', onboardingController.submitApplication); router.post('/apply', submitApplication);
// All subsequent routes require authentication // All subsequent routes require authentication
router.use(authenticate as any); router.use(authenticate as any);
router.get('/applications', onboardingController.getApplications); router.get('/applications', getApplications);
router.post('/applications/shortlist', onboardingController.bulkShortlist); router.post('/applications/shortlist', bulkShortlist); // Existing route, updated to named import
router.get('/applications/:id', onboardingController.getApplicationById); router.get('/applications/:id', getApplicationById);
router.put('/applications/:id/status', onboardingController.updateApplicationStatus); router.put('/applications/:id/status', updateApplicationStatus);
router.put('/applications/:id/status', onboardingController.updateApplicationStatus); router.post('/applications/:id/documents', uploadSingle, uploadDocuments);
router.post('/applications/:id/documents', uploadSingle, onboardingController.uploadDocuments); router.get('/applications/:id/documents', getApplicationDocuments); // Existing route, updated to named import
router.get('/applications/:id/documents', onboardingController.getApplicationDocuments);
// Architecture-related routes
router.post('/applications/:id/assign-architecture', assignArchitectureTeam);
router.put('/applications/:id/architecture-status', updateArchitectureStatus);
// Questionnaire Routes // Questionnaire Routes
router.get('/questionnaires', (req, res, next) => { router.get('/questionnaires', (req, res, next) => {

View File

@ -24,12 +24,12 @@ export const submitRequest = async (req: AuthRequest, res: Response) => {
description: reason, description: reason,
currentStage: 'DD_ADMIN_REVIEW' as any, currentStage: 'DD_ADMIN_REVIEW' as any,
status: 'Pending', status: 'Pending',
progressPercentage: 0, progressPercentage: 20,
documents: [], documents: [],
timeline: [{ timeline: [{
stage: 'Submitted', stage: 'Submitted',
timestamp: new Date(), timestamp: new Date(),
user: req.user.name, user: req.user.fullName,
action: 'Request submitted' action: 'Request submitted'
}] }]
}); });
@ -65,7 +65,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
{ {
model: User, model: User,
as: 'dealer', as: 'dealer',
attributes: ['name'] attributes: ['fullName']
} }
], ],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
@ -97,7 +97,7 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
{ {
model: User, model: User,
as: 'dealer', as: 'dealer',
attributes: ['name', 'email'] attributes: ['fullName', 'email']
}, },
{ {
model: Worknote, 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' }); 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, { const timeline = [...request.timeline, {
stage: 'Review', stage: currentStage,
timestamp: new Date(), timestamp: new Date(),
user: req.user.name, user: req.user.fullName,
action, action,
remarks: comments remarks: comments
}]; }];
await request.update({ await request.update({
status: action, status: finalStatus,
currentStage: nextStage as any,
timeline, timeline,
updatedAt: new Date() updatedAt: new Date()
}); });

View File

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

View File

@ -18,8 +18,11 @@ const calculateProgress = (stage: string): number => {
[RESIGNATION_STAGES.RBM]: 30, [RESIGNATION_STAGES.RBM]: 30,
[RESIGNATION_STAGES.ZBH]: 45, [RESIGNATION_STAGES.ZBH]: 45,
[RESIGNATION_STAGES.NBH]: 60, [RESIGNATION_STAGES.NBH]: 60,
[RESIGNATION_STAGES.DD_ADMIN]: 70, [RESIGNATION_STAGES.DD_ADMIN]: 65,
[RESIGNATION_STAGES.LEGAL]: 80, [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.FINANCE]: 90,
[RESIGNATION_STAGES.FNF_INITIATED]: 95, [RESIGNATION_STAGES.FNF_INITIATED]: 95,
[RESIGNATION_STAGES.COMPLETED]: 100, [RESIGNATION_STAGES.COMPLETED]: 100,
@ -88,7 +91,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N
timeline: [{ timeline: [{
stage: 'Submitted', stage: 'Submitted',
timestamp: new Date(), timestamp: new Date(),
user: req.user.name, user: req.user.fullName,
action: 'Resignation request submitted' action: 'Resignation request submitted'
}] }]
}, { transaction }); }, { transaction });
@ -260,7 +263,10 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH, [RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN, [RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL, [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.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED,
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED [RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED
}; };
@ -279,7 +285,7 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
const timeline = [...resignation.timeline, { const timeline = [...resignation.timeline, {
stage: nextStage, stage: nextStage,
timestamp: new Date(), timestamp: new Date(),
user: req.user.name, user: req.user.fullName,
action: 'Approved', action: 'Approved',
remarks remarks
}]; }];
@ -357,7 +363,7 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
const timeline = [...resignation.timeline, { const timeline = [...resignation.timeline, {
stage: 'Rejected', stage: 'Rejected',
timestamp: new Date(), timestamp: new Date(),
user: req.user.name, user: req.user.fullName,
action: 'Rejected', action: 'Rejected',
reason reason
}]; }];
@ -399,3 +405,39 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
next(error); 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.get('/:id', authenticate as any, resignationController.getResignationById);
router.put('/:id/approve', authenticate as any, resignationController.approveResignation); router.put('/:id/approve', authenticate as any, resignationController.approveResignation);
router.put('/:id/reject', authenticate as any, resignationController.rejectResignation); router.put('/:id/reject', authenticate as any, resignationController.rejectResignation);
router.put('/:id/clearance', authenticate as any, resignationController.updateClearance);
export default router; 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 auditRoutes from './modules/audit/audit.routes.js';
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js'; import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
import prospectiveLoginRoutes from './modules/prospective-login/prospective-login.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 common middleware & utils
import errorHandler from './common/middleware/errorHandler.js'; import errorHandler from './common/middleware/errorHandler.js';
@ -125,6 +126,7 @@ app.use('/api/communication', communicationRoutes);
app.use('/api/audit', auditRoutes); app.use('/api/audit', auditRoutes);
app.use('/api/questionnaire', questionnaireRoutes); app.use('/api/questionnaire', questionnaireRoutes);
app.use('/api/prospective-login', prospectiveLoginRoutes); app.use('/api/prospective-login', prospectiveLoginRoutes);
app.use('/api/termination', terminationRoutes);
// Backward Compatibility Aliases // Backward Compatibility Aliases
app.use('/api/applications', onboardingRoutes); app.use('/api/applications', onboardingRoutes);