From 934fd7a907eb685e8a2694cb0dea86b09b521ce8 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 10 Apr 2026 09:20:04 +0530 Subject: [PATCH] participants mappig done for other modules --- all_ids.txt | Bin 0 -> 4100 bytes drop_constraint.ts | 16 ++ fix_missing_participants.ts | 32 +++ quick_query.ts | 23 ++ src/common/config/constants.ts | 3 +- src/database/models/ConstitutionalChange.ts | 5 + src/database/models/Resignation.ts | 5 + src/database/models/TerminationRequest.ts | 5 + .../self-service/constitutional.controller.ts | 12 +- .../self-service/resignation.controller.ts | 41 ++++ .../self-service/resignation.routes.ts | 1 + .../termination/termination.controller.ts | 13 +- src/modules/termination/termination.routes.ts | 1 + src/services/ParticipantService.ts | 213 ++++++++++++++++++ trigger-workflow.js | 4 +- verify_output.txt | Bin 0 -> 164196 bytes verify_participants_logic.ts | 55 +++++ 17 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 all_ids.txt create mode 100644 drop_constraint.ts create mode 100644 fix_missing_participants.ts create mode 100644 quick_query.ts create mode 100644 src/services/ParticipantService.ts create mode 100644 verify_output.txt create mode 100644 verify_participants_logic.ts diff --git a/all_ids.txt b/all_ids.txt new file mode 100644 index 0000000000000000000000000000000000000000..269b9606f7c9c1d10fabe2bd52c6f33357e8d8f5 GIT binary patch literal 4100 zcmdUyZ*Lk$5XR?qrGAG~6xB{zL5z*<&_7b*+EOCdrFN=TDlNf)G3|jVV8>~ce)Ocz zZ|9c7aE=h~s?%|Mdpk4FJTtR9eEcx9E9+R-O1f`#CsxxPXKU8ic&hoao-IqQY$J`Y ztZkp$XPTerigN?4o`kuYZQE1Z32UB7KCuJoZCE|X_VnAbfo9QhB&jq=HD-4Ilpfwj z!c@}l#I__~kxrLs+od#&HMbpfe5rY^FLXb&6Z_G=w?k{{{#Ccn)irJk$FaT8^QE0> z9{Uf2rk-r@7Bw;cckK&lN0bDO2eJW+Jux@7w?Z(q3ytHNyU$#jSe$6xRA@(%Z;Pu7 z-Cf0uZ<{m5JW}Y1(#27`KVpu-r9FBzS)B?mInc95(*2wLZeQtnDg=Xot1EtQHIqxX z^=%}%zP=Aq!owv1IHzu()HpwwED?&qoCu_9`+9&sO&e>FC*>cFSFPShXhw9yv<58i_q}lPV(vQt{+WPo(P`aZdh^#HHuY zcZxOA#CyG1BTbxEXJwudq>1oRZ^_+O8Ks_h49GQU#D!I{|gI-4r~)O<^f9BD)za7J`AA{+j%c@R0E%h+3i z15Q8o=jdcNH6vsIZ8UjPa$+K z93}Zi2Uo<*Y4ETkFWoD;Ku5I95js~#;2hE6V7(grDMC!K^r9Z;LlLEZH&b@UM#R%vEVC0qVTWMtNYt3=K(QCyT-w-?W;aI*A zQoN*x?+eF4z)kgid63tb@YjStDAgisqZ9f zqSts)(weUQt@R-5+dq1ivUmQ=Yzozxy1--I2jVfY2bnhPxEz-0=^MIU-xDqNxPc_- zKj=27r~9&z=;DEQ^=NnRqr_rvzb~-Ko=rSsMe)yOQEnuUu|kgU?*n|%#`0>k#T?rB zM7NC<-y_jWrwp%1^RFNkpYu50&)(({5-0wXyN28cBwd2hiF~B)J$n*mZber~HMyf- H?*jh=J2ZKa literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f36149d7f741e121b673c89387d52fac35d5e47d GIT binary patch literal 164196 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