import db from '../database/models/index.js'; const { RelocationRequest, AuditLog, User } = db; import { AUDIT_ACTIONS, RELOCATION_STAGES, ROLES, REQUEST_TYPES } from '../common/config/constants.js'; import { getOffboardingAuditAction, formatOffboardingAction } from '../common/utils/offboardingWorkflow.utils.js'; import { writeWorkflowActivityWorknote } from '../common/utils/workflowWorknote.js'; import logger from '../common/utils/logger.js'; import { NotificationService } from './NotificationService.js'; export class RelocationWorkflowService { /** * Standardized method to transition a relocation request status */ static async transitionRelocation(request: any, targetStatus: string, userId: string | null = null, metadata: any = {}) { const previousStatus = request.status; const { reason, stage, progressPercentage, action, auditAction } = metadata; const updateData: any = { status: targetStatus, updatedAt: new Date() }; // Update stage if provided and valid if (stage && Object.values(RELOCATION_STAGES).includes(stage)) { updateData.currentStage = stage; } // Update progress percentage if explicitly provided if (progressPercentage !== undefined) { updateData.progressPercentage = progressPercentage; } const sourceStage = request.currentStage; // 1. Update Request Record await request.update(updateData); // 2. Update Timeline (JSON array) const user = userId ? await User.findByPk(userId) : null; const timelineEntry = { stage: sourceStage, // Store the stage where the action happened targetStage: stage || targetStatus, timestamp: new Date(), user: user ? user.fullName : 'System', action: action || `Transitioned to ${targetStatus}`, remarks: reason || '' }; const updatedTimeline = [...(request.timeline || []), timelineEntry]; await request.update({ timeline: updatedTimeline }); // 3. Create Audit Log using standardized mapper const { actionType } = metadata; let resolvedAuditAction = getOffboardingAuditAction(actionType || action || 'Approved', REQUEST_TYPES.RELOCATION); if (auditAction) { resolvedAuditAction = auditAction; } else if (action === 'REJECT') { resolvedAuditAction = AUDIT_ACTIONS.REJECTED; } else if (action === 'REVOKE') { resolvedAuditAction = AUDIT_ACTIONS.RELOCATION_REVOKED; } else if (action === 'SEND_BACK') { resolvedAuditAction = AUDIT_ACTIONS.RELOCATION_SENT_BACK; } await db.RelocationAudit.create({ userId: userId, relocationRequestId: request.id, action: formatOffboardingAction(resolvedAuditAction), remarks: reason || '', details: { status: targetStatus, stage: sourceStage, targetStage: stage || targetStatus } }); // 4. Create Worknote for standardized communication trail if (reason && userId) { try { // Prepend action to remarks for better context in Work Notes const actionPrefix = action ? `${formatOffboardingAction(action)}: ` : ''; await writeWorkflowActivityWorknote({ requestId: request.id, requestType: 'relocation', userId: userId, noteText: `${actionPrefix}${reason}`, noteType: (action === 'SEND_BACK' || action === 'SEND_BACK_TO_ASM') ? 'workflow' : 'internal' }); } catch (wnErr) { logger.error('[RelocationWorkflowService] failed to write worknote:', wnErr); } } console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`); await request.reload(); const dealerUser = await User.findByPk(request.dealerId, { attributes: ['id', 'email', 'fullName'] }); if (dealerUser?.email) { const portalBase = process.env.FRONTEND_URL || 'http://localhost:5173'; const stageLabel = request.currentStage || request.status || targetStatus; const { notifyStakeholdersOnTransition } = await import('../common/utils/workflow-email-notifications.js'); await notifyStakeholdersOnTransition( request.id, REQUEST_TYPES.RELOCATION, stageLabel, { code: request.requestId, dealerName: dealerUser.fullName || 'Dealer', dealerId: dealerUser.id, actionUserFullName: user ? user.fullName : 'System', action: action || `Transitioned to ${targetStatus}`, remarks: reason || 'N/A', link: `${portalBase}/relocation-requests/${request.id}` } ); } return request; } /** * Checks if a user is authorized to perform an action in the current stage */ static async canUserAction(request: any, user: any) { if (!user) return false; // Super Admin bypass if (user.roleCode === ROLES.SUPER_ADMIN) return true; const stageMapping: Record = { [RELOCATION_STAGES.ASM_REVIEW]: ROLES.ASM, 'DD Admin Review': ROLES.ASM, // Legacy/alias mapping for older requests [RELOCATION_STAGES.RBM_REVIEW]: ROLES.RBM, [RELOCATION_STAGES.DD_ZM_REVIEW]: ROLES.DD_ZM, [RELOCATION_STAGES.ZBH_REVIEW]: ROLES.ZBH, [RELOCATION_STAGES.DD_LEAD_REVIEW]: ROLES.DD_LEAD, [RELOCATION_STAGES.DD_HEAD_APPROVAL]: ROLES.DD_HEAD, [RELOCATION_STAGES.NBH_APPROVAL]: ROLES.NBH, [RELOCATION_STAGES.LEGAL_CLEARANCE]: ROLES.LEGAL_ADMIN, [RELOCATION_STAGES.NBH_CLEARANCE_EOR]: ROLES.NBH }; const requiredRole = stageMapping[request.currentStage]; if (!requiredRole) return false; // Role-based check if (user.roleCode !== requiredRole) return false; // Optional: Hierarchy check // We could verify if the user is the SPECIFIC person assigned in participants // but for now, any user with the correct role can act (consistent with simple RBAC) return true; } }