451 lines
16 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
|
|
}
|