diff --git a/all_ids.txt b/all_ids.txt new file mode 100644 index 0000000..269b960 Binary files /dev/null and b/all_ids.txt differ diff --git a/drop_constraint.ts b/drop_constraint.ts new file mode 100644 index 0000000..bbdc627 --- /dev/null +++ b/drop_constraint.ts @@ -0,0 +1,16 @@ +import 'dotenv/config'; +import db from './src/database/models/index.js'; + +async function run() { + try { + console.log('Attempting to drop constraint request_participants_requestId_fkey...'); + await db.sequelize.query('ALTER TABLE request_participants DROP CONSTRAINT IF EXISTS "request_participants_requestId_fkey"'); + console.log('Constraint dropped successfully.'); + process.exit(0); + } catch (error) { + console.error('Error dropping constraint:', error); + process.exit(1); + } +} + +run(); diff --git a/fix_missing_participants.ts b/fix_missing_participants.ts new file mode 100644 index 0000000..f76bc76 --- /dev/null +++ b/fix_missing_participants.ts @@ -0,0 +1,32 @@ +import { ParticipantService } from './src/services/ParticipantService.js'; + +async function run() { + try { + const requestId = '29b742a7-6d9f-4736-8aae-295ffe32ef75'; + console.log(`Fixing participants for resignation ${requestId}...`); + + const { Resignation, User, Dealer, Application, District } = (await import('./src/database/models/index.js')).default; + const resignation = await Resignation.findByPk(requestId); + console.log('Resignation Record:', JSON.stringify(resignation, null, 2)); + + if (resignation) { + const user = await User.findByPk(resignation.dealerId); + console.log('User Record:', JSON.stringify(user, null, 2)); + if (user && user.dealerId) { + const dealer = await Dealer.findByPk(user.dealerId, { + include: [{ model: Application, as: 'application', include: [{ model: District, as: 'district' }] }] + }); + console.log('Dealer/Application/District Record:', JSON.stringify(dealer, null, 2)); + } + } + + await ParticipantService.assignResignationParticipants(requestId); + console.log('Done.'); + process.exit(0); + } catch (error) { + console.error('Error fixing participants:', error); + process.exit(1); + } +} + +run(); diff --git a/quick_query.ts b/quick_query.ts new file mode 100644 index 0000000..13efcb5 --- /dev/null +++ b/quick_query.ts @@ -0,0 +1,23 @@ +import 'dotenv/config'; +import db from './src/database/models/index.js'; + +async function run() { + try { + const participants = await db.RequestParticipant.findAll({ + limit: 100, + include: [{ model: db.User, as: 'user', attributes: ['fullName', 'roleCode', 'email'] }] + }); + + console.log(`Found ${participants.length} participants in total:`); + participants.forEach((p: any) => { + console.log(`- RequestId: ${p.requestId}, Type: ${p.requestType}, User: ${p.user.fullName}`); + }); + + process.exit(0); + } catch (error) { + console.error('Error querying participants:', error); + process.exit(1); + } +} + +run(); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 8242d4a..3c9e1d7 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -408,7 +408,8 @@ export const REQUEST_TYPES = { APPLICATION: 'application', RESIGNATION: 'resignation', CONSTITUTIONAL: 'constitutional', - RELOCATION: 'relocation' + RELOCATION: 'relocation', + TERMINATION: 'termination' } as const; // Module List for Document Management diff --git a/src/database/models/ConstitutionalChange.ts b/src/database/models/ConstitutionalChange.ts index 6d803ae..852b3bf 100644 --- a/src/database/models/ConstitutionalChange.ts +++ b/src/database/models/ConstitutionalChange.ts @@ -123,6 +123,11 @@ export default (sequelize: Sequelize) => { scope: { requestType: 'constitutional' }, constraints: false }); + ConstitutionalChange.hasMany(models.RequestParticipant, { + foreignKey: 'requestId', + as: 'participants', + scope: { requestType: 'constitutional' } + }); }; return ConstitutionalChange; diff --git a/src/database/models/Resignation.ts b/src/database/models/Resignation.ts index 47fe304..2c188ed 100644 --- a/src/database/models/Resignation.ts +++ b/src/database/models/Resignation.ts @@ -147,6 +147,11 @@ export default (sequelize: Sequelize) => { foreignKey: 'resignationId', as: 'settlement' }); + Resignation.hasMany(models.RequestParticipant, { + foreignKey: 'requestId', + as: 'participants', + scope: { requestType: 'resignation' } + }); }; return Resignation; diff --git a/src/database/models/TerminationRequest.ts b/src/database/models/TerminationRequest.ts index 12c8c64..f1be060 100644 --- a/src/database/models/TerminationRequest.ts +++ b/src/database/models/TerminationRequest.ts @@ -111,6 +111,11 @@ export default (sequelize: Sequelize) => { scope: { requestType: 'termination' }, constraints: false }); + TerminationRequest.hasMany(models.RequestParticipant, { + foreignKey: 'requestId', + as: 'participants', + scope: { requestType: 'termination' } + }); }; return TerminationRequest; diff --git a/src/modules/self-service/constitutional.controller.ts b/src/modules/self-service/constitutional.controller.ts index 6f1d3ab..f70c8bd 100644 --- a/src/modules/self-service/constitutional.controller.ts +++ b/src/modules/self-service/constitutional.controller.ts @@ -6,6 +6,7 @@ import { AuthRequest } from '../../types/express.types.js'; import { CONSTITUTIONAL_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js'; import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWorkflowService.js'; import { NomenclatureService } from '../../common/utils/nomenclature.js'; +import { ParticipantService } from '../../services/ParticipantService.js'; export const submitRequest = async (req: AuthRequest, res: Response) => { try { @@ -49,6 +50,10 @@ export const submitRequest = async (req: AuthRequest, res: Response) => { entityId: request.id }); + // Add as chat participants (Async) + ParticipantService.assignConstitutionalParticipants(request.id) + .catch(err => console.error('Error assigning participants to constitutional change:', err)); + res.status(201).json({ success: true, message: 'Constitutional change request submitted successfully', @@ -96,7 +101,12 @@ export const getRequestById = async (req: AuthRequest, res: Response) => { include: [ { model: Outlet, as: 'outlet' }, { model: User, as: 'dealer', attributes: ['fullName', 'email'] }, - { model: Worknote, as: 'worknotes' } + { model: Worknote, as: 'worknotes' }, + { + model: db.RequestParticipant, + as: 'participants', + include: [{ model: User, as: 'user', attributes: ['id', 'fullName', 'email', 'roleCode'] }] + } ] }); diff --git a/src/modules/self-service/resignation.controller.ts b/src/modules/self-service/resignation.controller.ts index b091041..084baaa 100644 --- a/src/modules/self-service/resignation.controller.ts +++ b/src/modules/self-service/resignation.controller.ts @@ -8,6 +8,7 @@ import ExternalMocksService from '../../common/utils/externalMocks.service.js'; import { ResignationWorkflowService } from '../../services/ResignationWorkflowService.js'; import { NomenclatureService } from '../../common/utils/nomenclature.js'; +import { ParticipantService } from '../../services/ParticipantService.js'; // Removed generateResignationId and moved to NomenclatureService @@ -73,6 +74,11 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N await transaction.commit(); logger.info(`Resignation request created: ${resignationId} by dealer: ${req.user.email}`); + + // Add as chat participants (Async) + ParticipantService.assignResignationParticipants(resignation.id) + .catch(err => logger.error('Error assigning participants to resignation:', err)); + res.status(201).json({ success: true, message: 'Resignation request submitted successfully', resignationId, resignation }); } catch (error) { if (transaction) await transaction.rollback(); @@ -127,6 +133,11 @@ export const getResignationById = async (req: AuthRequest, res: Response, next: { model: db.FnFLineItem, as: 'lineItems' }, { model: db.FffClearance, as: 'clearances' } ] + }, + { + model: db.RequestParticipant, + as: 'participants', + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'roleCode'] }] } ] }); @@ -505,3 +516,33 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex next(error); } }; +// Unified status update handler for frontend compatibility +export const updateResignationStatus = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { action } = req.body; + + switch (action) { + case 'approve': + return approveResignation(req, res, next); + case 'reject': + return rejectResignation(req, res, next); + case 'withdrawal': + case 'withdraw': + return withdrawResignation(req, res, next); + case 'sendback': + return sendBackResignation(req, res, next); + case 'pushfnf': + // For push to F&F, we can trigger the same logic as Legal -> FNF_INITIATED approve + // But specifically for the pushfnf button action + return approveResignation(req, res, next); + default: + return res.status(400).json({ + success: false, + message: `Invalid or unsupported resignation action: ${action}` + }); + } + } catch (error) { + logger.error('Error in updateResignationStatus:', error); + next(error); + } +}; diff --git a/src/modules/self-service/resignation.routes.ts b/src/modules/self-service/resignation.routes.ts index c8b5d10..b016647 100644 --- a/src/modules/self-service/resignation.routes.ts +++ b/src/modules/self-service/resignation.routes.ts @@ -10,6 +10,7 @@ router.post('/', authenticate as any, resignationController.createResignation); 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.post('/:id/status', authenticate as any, resignationController.updateResignationStatus); router.put('/:id/reject', authenticate as any, resignationController.rejectResignation); router.put('/:id/withdraw', authenticate as any, resignationController.withdrawResignation); router.put('/:id/sendback', authenticate as any, resignationController.sendBackResignation); diff --git a/src/modules/termination/termination.controller.ts b/src/modules/termination/termination.controller.ts index 80a2bb6..1aa9d44 100644 --- a/src/modules/termination/termination.controller.ts +++ b/src/modules/termination/termination.controller.ts @@ -8,6 +8,7 @@ import ExternalMocksService from '../../common/utils/externalMocks.service.js'; import { TerminationWorkflowService } from '../../services/TerminationWorkflowService.js'; import { NomenclatureService } from '../../common/utils/nomenclature.js'; +import { ParticipantService } from '../../services/ParticipantService.js'; // Create termination request export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => { @@ -45,6 +46,11 @@ export const createTermination = async (req: AuthRequest, res: Response, next: N }, { transaction }); await transaction.commit(); + + // Add as chat participants (Async) + ParticipantService.assignTerminationParticipants(termination.id) + .catch(err => logger.error('Error assigning participants to termination:', err)); + res.status(201).json({ success: true, message: 'Termination request created', termination }); } catch (error) { if (transaction) await transaction.rollback(); @@ -110,7 +116,12 @@ export const getTerminationById = async (req: AuthRequest, res: Response, next: as: 'uploadedDocuments', include: [{ model: db.User, as: 'uploader', attributes: ['fullName'] }] }, - { model: db.FnF, as: 'fnfSettlement' } + { model: db.FnF, as: 'fnfSettlement' }, + { + model: db.RequestParticipant, + as: 'participants', + include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'roleCode'] }] + } ] }); diff --git a/src/modules/termination/termination.routes.ts b/src/modules/termination/termination.routes.ts index 640379c..334dd5c 100644 --- a/src/modules/termination/termination.routes.ts +++ b/src/modules/termination/termination.routes.ts @@ -12,6 +12,7 @@ router.post('/', createTermination); router.get('/', getTerminations); router.get('/:id', getTerminationById); router.put('/:id/status', updateTerminationStatus); +router.post('/:id/status', updateTerminationStatus); router.post('/scn-response', submitScnResponse); router.post('/hearing-record', recordPersonalHearing); diff --git a/src/services/ParticipantService.ts b/src/services/ParticipantService.ts new file mode 100644 index 0000000..209769e --- /dev/null +++ b/src/services/ParticipantService.ts @@ -0,0 +1,213 @@ +import db from '../database/models/index.js'; +import { ROLES, REQUEST_TYPES } from '../common/config/constants.js'; +import { Op } from 'sequelize'; + +const { + RequestParticipant, + User, + TerminationRequest, + ConstitutionalChange, + Dealer, + Application, + District, + Region, + Zone +} = 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 { + await RequestParticipant.findOrCreate({ + where: { + requestId, + requestType, + userId + }, + defaults: { + 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: district.asmId, + zmId: district.zmId, + rbmId: district.region?.rbmId, + zbhId: district.zone?.zbhId + }; + } + + /** + * Assign participants for Termination Request + */ + static async assignTerminationParticipants(terminationId: string) { + try { + const termination = await TerminationRequest.findByPk(terminationId); + if (!termination) return; + + // TerminationRequest already uses Dealer ID + const managers = await this.getDealerLocationManagers(termination.dealerId); + const participantIds = new Set(); + + // 1. Location based managers + if (managers) { + if (managers.rbmId) participantIds.add(managers.rbmId); + if (managers.zbhId) participantIds.add(managers.zbhId); + } + + // 2. National roles + const nationalRoles = [ROLES.DD_LEAD, ROLES.NBH, ROLES.CCO, ROLES.CEO, ROLES.LEGAL_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 + for (const userId of participantIds) { + await this.addParticipant(termination.id, REQUEST_TYPES.TERMINATION, userId); + } + + console.log(`[ParticipantService] Added ${participantIds.size} participants to termination ${terminationId}`); + } catch (error) { + console.error('Error assigning termination participants:', error); + } + } + + /** + * Assign participants for Constitutional Change Request + */ + static async assignConstitutionalParticipants(requestId: string) { + try { + const request = await ConstitutionalChange.findByPk(requestId); + if (!request) return; + + // In ConstitutionalChange model, dealerId is the User ID + const user = await User.findByPk(request.dealerId); + if (!user || !user.dealerId) { + console.error(`[ParticipantService] No Dealer ID found for user ${request.dealerId}`); + return; + } + + const managers = await this.getDealerLocationManagers(user.dealerId); + const participantIds = new Set(); + + // 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.LEGAL_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 + for (const userId of participantIds) { + await this.addParticipant(request.id, REQUEST_TYPES.CONSTITUTIONAL, userId); + } + + console.log(`[ParticipantService] Added ${participantIds.size} 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 { + const resignation = await db.Resignation.findByPk(requestId); + if (!resignation) return; + + // In Resignation model, dealerId is the User ID + const user = await User.findByPk(resignation.dealerId); + if (!user || !user.dealerId) { + console.error(`[ParticipantService] No Dealer ID found for user ${resignation.dealerId}`); + return; + } + + const managers = await this.getDealerLocationManagers(user.dealerId); + const participantIds = new Set(); + + // 1. Location based managers + 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.NBH, ROLES.DD_ADMIN, ROLES.FINANCE, ROLES.LEGAL_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 + for (const userId of participantIds) { + await this.addParticipant(resignation.id, REQUEST_TYPES.RESIGNATION, userId); + } + + console.log(`[ParticipantService] Added ${participantIds.size} participants to resignation ${requestId}`); + } catch (error) { + console.error('Error assigning resignation participants:', error); + } + } +} diff --git a/trigger-workflow.js b/trigger-workflow.js index d4babbe..012ee2c 100644 --- a/trigger-workflow.js +++ b/trigger-workflow.js @@ -94,7 +94,7 @@ async function prospectLogin(phone) { async function mockUploadDocument(appId, token, docType) { const formData = new FormData(); - const fileBuffer = fs.readFileSync('/home/laxman-h/Pictures/Screenshots/Screenshot from 2026-03-27 09-48-22.png'); + const fileBuffer = fs.readFileSync('C:/Users/BACKPACKERS/Pictures/claim_document_type.PNG'); const blob = new Blob([fileBuffer], { type: 'image/png' }); formData.append('file', blob, 'screenshot.png'); formData.append('documentType', docType); @@ -410,7 +410,7 @@ async function triggerWorkflow() { // 12. FINAL ONBOARDING log(12, 'Admin Finalizing Dealer Onboarding...'); await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken); - + log(12, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---'); log(12, `The application ${applicationId} is now at 'ONBOARDED' status.`); } diff --git a/verify_output.txt b/verify_output.txt new file mode 100644 index 0000000..f36149d Binary files /dev/null and b/verify_output.txt differ diff --git a/verify_participants_logic.ts b/verify_participants_logic.ts new file mode 100644 index 0000000..094451e --- /dev/null +++ b/verify_participants_logic.ts @@ -0,0 +1,55 @@ +import db from './src/database/models/index.js'; +import { ParticipantService } from './src/services/ParticipantService.js'; + +async function verify() { + try { + console.log('--- Verification Started ---'); + + // 1. Verify Termination Participants + const termination = await db.TerminationRequest.findOne(); + if (termination) { + console.log(`Found termination ${termination.id}. Assigning participants...`); + await ParticipantService.assignTerminationParticipants(termination.id); + const participants = await db.RequestParticipant.findAll({ + where: { requestId: termination.id, requestType: 'termination' } + }); + console.log(`Termination Participants added: ${participants.length}`); + } else { + console.log('No termination records found to verify.'); + } + + // 2. Verify Constitutional Participants + const constitutional = await db.ConstitutionalChange.findOne(); + if (constitutional) { + console.log(`Found constitutional change ${constitutional.id}. Assigning participants...`); + await ParticipantService.assignConstitutionalParticipants(constitutional.id); + const participants = await db.RequestParticipant.findAll({ + where: { requestId: constitutional.id, requestType: 'constitutional' } + }); + console.log(`Constitutional Participants added: ${participants.length}`); + } else { + console.log('No constitutional change records found to verify.'); + } + + // 3. Verify Resignation Participants + const resignation = await db.Resignation.findOne(); + if (resignation) { + console.log(`Found resignation ${resignation.id}. Assigning participants...`); + await ParticipantService.assignResignationParticipants(resignation.id); + const participants = await db.RequestParticipant.findAll({ + where: { requestId: resignation.id, requestType: 'resignation' } + }); + console.log(`Resignation Participants added: ${participants.length}`); + } else { + console.log('No resignation records found to verify.'); + } + + console.log('--- Verification Completed ---'); + process.exit(0); + } catch (error) { + console.error('Verification failed:', error); + process.exit(1); + } +} + +verify();