import db from '../database/models/index.js'; const { RelocationRequest, AuditLog, User } = db; import { AUDIT_ACTIONS, RELOCATION_STAGES, ROLES } from '../common/config/constants.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 let resolvedAuditAction: string = AUDIT_ACTIONS.APPROVED; 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: resolvedAuditAction, remarks: reason || '', details: { status: targetStatus, stage: sourceStage, targetStage: stage || targetStatus } }); console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`); 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; } }