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