Dealer_Onboarding_Backend/src/services/ParticipantService.ts

451 lines
16 KiB
TypeScript

import db from '../database/models/index.js';
import { ROLES, REQUEST_TYPES } from '../common/config/constants.js';
import { Op } from 'sequelize';
import { isAutoAssignmentEnabled } from './AutoAssignmentConfigService.js';
const {
RequestParticipant,
User,
TerminationRequest,
ConstitutionalChange,
Dealer,
Application,
District,
Region,
Zone,
Outlet,
RelocationRequest
} = db;
export class ParticipantService {
/**
* Common helper to add a participant
*/
private static async addParticipant(requestId: string, requestType: string, userId: string, participantType: string = 'contributor', metadata: any = {}) {
try {
const existing = await RequestParticipant.findOne({
where: {
requestId,
requestType,
userId
}
});
if (existing) {
// Requirement: Auto-assignment logic skips participants with revokedAt to prevent re-adding
if (existing.metadata?.revokedAt) {
console.log(`[ParticipantService] Skipping revoked participant ${userId} for ${requestType} ${requestId}`);
return;
}
return; // Already exists
}
await RequestParticipant.create({
requestId,
requestType,
userId,
participantType,
joinedMethod: 'auto',
metadata: {
...metadata,
autoMapped: true,
assignedAt: new Date()
}
});
} catch (error) {
console.error(`Error adding participant ${userId} to ${requestType} ${requestId}:`, error);
}
}
/**
* Resolves location-based managers for a dealer
*/
private static async getDealerLocationManagers(dealerId: string) {
const dealer = await Dealer.findByPk(dealerId, {
include: [{
model: Application,
as: 'application',
include: [{
model: District,
as: 'district',
include: [
{ model: Region, as: 'region' },
{ model: Zone, as: 'zone' }
]
}]
}]
});
if (!dealer || !dealer.application || !dealer.application.district) {
return null;
}
const district = dealer.application.district;
return {
asmId: dealer.asmId || null,
zmId: district.zmId,
rbmId: district.region?.rbmId,
zbhId: district.zone?.zbhId
};
}
/**
* Assign participants for Termination Request
*/
static async assignTerminationParticipants(requestId: string) {
try {
if (!await isAutoAssignmentEnabled('TERMINATION')) {
console.log(`[ParticipantService] Auto-assignment disabled for TERMINATION. Skipping for ${requestId}`);
return;
}
const termination = await db.TerminationRequest.findByPk(requestId);
if (!termination) {
console.error(`[ParticipantService] Termination Request not found: ${requestId}`);
return;
}
const participantIds = new Set<string>();
// 0. The Dealer themselves (Affected Party) should be a participant
if (termination.dealerId) {
// In Termination, dealerId is likely the Dealer Profile ID,
// need to resolve to User ID for participants
const dealerUser = await User.findOne({
where: { dealerId: termination.dealerId, roleCode: ROLES.DEALER }
});
if (dealerUser) participantIds.add(dealerUser.id);
}
// 1. Location based managers
if (termination.dealerId) {
const managers = await this.getDealerLocationManagers(termination.dealerId);
if (managers) {
if (managers.asmId) participantIds.add(managers.asmId);
if (managers.rbmId) participantIds.add(managers.rbmId);
if (managers.zbhId) participantIds.add(managers.zbhId);
}
}
// 2. National roles
const nationalRoles = [
ROLES.DD_LEAD,
ROLES.DD_HEAD,
ROLES.NBH,
ROLES.CCO,
ROLES.CEO,
ROLES.DD_ADMIN,
ROLES.FINANCE,
ROLES.LEGAL_ADMIN,
ROLES.SUPER_ADMIN
];
const nationalUsers = await User.findAll({
where: {
roleCode: { [Op.in]: nationalRoles },
status: 'active'
},
attributes: ['id']
});
nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 3. Add all unique participants
let addedCount = 0;
for (const userId of participantIds) {
// Determine type (Dealer profile id is not userId)
// We'll check if the userId matches the resolved dealer user
const isDealer = termination.dealerId && (await User.findByPk(userId))?.dealerId === termination.dealerId;
const pType = isDealer ? 'owner' : 'contributor';
await this.addParticipant(termination.id, REQUEST_TYPES.TERMINATION, userId, pType);
addedCount++;
}
console.log(`[ParticipantService] Added ${addedCount} participants to termination ${requestId}`);
} catch (error) {
console.error('Error assigning termination participants:', error);
}
}
/**
* Assign participants for Constitutional Change Request
*/
static async assignConstitutionalParticipants(requestId: string) {
try {
if (!await isAutoAssignmentEnabled('CONSTITUTIONAL')) {
console.log(`[ParticipantService] Auto-assignment disabled for CONSTITUTIONAL. Skipping for ${requestId}`);
return;
}
const request = await ConstitutionalChange.findByPk(requestId);
if (!request) return;
const participantIds = new Set<string>();
// 0. The Dealer (Requester) should be a participant
if (request.dealerId) participantIds.add(request.dealerId);
// In ConstitutionalChange model, dealerId is the User ID
const user = await User.findByPk(request.dealerId);
if (user && user.dealerId) {
const managers = await this.getDealerLocationManagers(user.dealerId);
// 1. Location based managers
if (managers) {
if (managers.asmId) participantIds.add(managers.asmId);
if (managers.zmId) participantIds.add(managers.zmId);
if (managers.rbmId) participantIds.add(managers.rbmId);
if (managers.zbhId) participantIds.add(managers.zbhId);
}
}
// 2. National roles
const nationalRoles = [
ROLES.DD_LEAD,
ROLES.DD_HEAD,
ROLES.NBH,
ROLES.CCO,
ROLES.CEO,
ROLES.FINANCE,
ROLES.DD_ADMIN,
ROLES.LEGAL_ADMIN,
ROLES.SUPER_ADMIN
];
const nationalUsers = await User.findAll({
where: {
roleCode: { [Op.in]: nationalRoles },
status: 'active'
},
attributes: ['id']
});
nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 3. Add all unique participants
let addedCount = 0;
for (const userId of participantIds) {
await this.addParticipant(request.id, REQUEST_TYPES.CONSTITUTIONAL, userId);
addedCount++;
}
console.log(`[ParticipantService] Added ${addedCount} participants to constitutional change ${requestId}`);
} catch (error) {
console.error('Error assigning constitutional participants:', error);
}
}
/**
* Assign participants for Resignation Request
*/
static async assignResignationParticipants(requestId: string) {
try {
if (!await isAutoAssignmentEnabled('RESIGNATION')) {
console.log(`[ParticipantService] Auto-assignment disabled for RESIGNATION. Skipping for ${requestId}`);
return;
}
const resignation = await db.Resignation.findByPk(requestId);
if (!resignation) {
console.error(`[ParticipantService] Resignation not found: ${requestId}`);
return;
}
const participantIds = new Set<string>();
// 0. The Dealer themselves (Requester) should be a participant
if (resignation.dealerId) {
participantIds.add(resignation.dealerId);
}
// In Resignation model, dealerId is the User ID
const user = await User.findByPk(resignation.dealerId);
// 1. Try to get Location based managers if dealer profile exists
if (user && user.dealerId) {
const managers = await this.getDealerLocationManagers(user.dealerId);
if (managers) {
if (managers.asmId) participantIds.add(managers.asmId);
if (managers.rbmId) participantIds.add(managers.rbmId);
if (managers.zbhId) participantIds.add(managers.zbhId);
}
} else {
console.warn(`[ParticipantService] No Dealer Profile link found for user ${resignation.dealerId}. Only adding national roles.`);
}
// 2. National roles - Essential for workflow transparency
const nationalRoles = [
ROLES.DD_LEAD,
ROLES.DD_HEAD,
ROLES.NBH,
ROLES.CCO,
ROLES.CEO,
ROLES.DD_ADMIN,
ROLES.FINANCE,
ROLES.LEGAL_ADMIN,
ROLES.SUPER_ADMIN // Added Super Admin as observer
];
const nationalUsers = await User.findAll({
where: {
roleCode: { [Op.in]: nationalRoles },
status: 'active'
},
attributes: ['id']
});
nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 3. Add all unique participants
let addedCount = 0;
for (const userId of participantIds) {
// Dealer gets 'owner' type, others get 'contributor'
const pType = userId === resignation.dealerId ? 'owner' : 'contributor';
await this.addParticipant(resignation.id, REQUEST_TYPES.RESIGNATION, userId, pType);
addedCount++;
}
console.log(`[ParticipantService] Added ${addedCount} participants to resignation ${requestId}`);
} catch (error) {
console.error('Error assigning resignation participants:', error);
}
}
/**
* Assign participants for Relocation Request
*/
static async assignRelocationParticipants(requestId: string) {
try {
if (!await isAutoAssignmentEnabled('RELOCATION')) {
console.log(`[ParticipantService] Auto-assignment disabled for RELOCATION. Skipping for ${requestId}`);
return;
}
const relocation = await db.RelocationRequest.findByPk(requestId, {
include: [{
model: Outlet,
as: 'outlet',
include: [{
model: District,
as: 'district',
include: [
{ model: Region, as: 'region' },
{ model: Zone, as: 'zone' }
]
}]
}]
});
if (!relocation) {
console.error(`[ParticipantService] Relocation not found: ${requestId}`);
return;
}
const participantIds = new Set<string>();
// 0. The Dealer (Requester)
if (relocation.dealerId) {
participantIds.add(relocation.dealerId);
}
// 1. Location-based managers from Outlet
const outlet = (relocation as any).outlet;
if (outlet && outlet.district) {
const district = outlet.district;
if (relocation.dealerId) {
const dealerUser = await User.findByPk(relocation.dealerId, { attributes: ['dealerId'] });
if (dealerUser?.dealerId) {
const dealerProfile = await Dealer.findByPk(dealerUser.dealerId, { attributes: ['asmId'] });
if (dealerProfile?.asmId) participantIds.add(dealerProfile.asmId);
}
}
if (district.zmId) participantIds.add(district.zmId);
if (district.region?.rbmId) participantIds.add(district.region.rbmId);
if (district.zone?.zbhId) participantIds.add(district.zone.zbhId);
}
// 2. National roles
const nationalRoles = [
ROLES.DD_LEAD,
ROLES.DD_HEAD,
ROLES.NBH,
ROLES.CCO,
ROLES.CEO,
ROLES.DD_ADMIN,
ROLES.LEGAL_ADMIN,
ROLES.SUPER_ADMIN
];
const nationalUsers = await User.findAll({
where: {
roleCode: { [Op.in]: nationalRoles },
status: 'active'
},
attributes: ['id']
});
nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 3. Add all unique participants
let addedCount = 0;
for (const userId of participantIds) {
const pType = userId === relocation.dealerId ? 'owner' : 'contributor';
await this.addParticipant(relocation.id, REQUEST_TYPES.RELOCATION, userId, pType);
addedCount++;
}
console.log(`[ParticipantService] Added ${addedCount} participants to relocation ${requestId}`);
} catch (error) {
console.error('Error assigning relocation participants:', error);
}
}
/**
* Assign participants for F&F Settlement (Sub-application)
*/
/**
* Assign participants for F&F Settlement (Sub-application) - Strictly limited to 8 National Roles
*/
static async assignFnFParticipants(fnfId: string) {
try {
if (!await isAutoAssignmentEnabled('FNF')) {
console.log(`[ParticipantService] Auto-assignment disabled for FNF. Skipping for ${fnfId}`);
return;
}
const fnf = await db.FnF.findByPk(fnfId);
if (!fnf) return;
const participantIds = new Set<string>();
// 1. National roles ONLY (Requested by user)
const nationalRoles = [
ROLES.DD_LEAD,
ROLES.DD_HEAD,
ROLES.NBH,
ROLES.CCO,
ROLES.CEO,
ROLES.FINANCE,
ROLES.LEGAL_ADMIN,
ROLES.SUPER_ADMIN
];
const nationalUsers = await User.findAll({
where: {
roleCode: { [Op.in]: nationalRoles },
status: 'active'
},
attributes: ['id']
});
nationalUsers.forEach((u: any) => participantIds.add(u.id));
// 2. Add all unique participants as contributors
let addedCount = 0;
for (const userId of participantIds) {
await this.addParticipant(fnf.id, REQUEST_TYPES.FNF, userId, 'contributor');
addedCount++;
}
console.log(`[ParticipantService] Added ${addedCount} participants to F&F settlement ${fnfId}`);
} catch (error) {
console.error('Error assigning F&F participants:', error);
}
}
}