153 lines
6.4 KiB
TypeScript
153 lines
6.4 KiB
TypeScript
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<string, string> = {
|
|
[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;
|
|
}
|
|
}
|