596 lines
27 KiB
TypeScript
596 lines
27 KiB
TypeScript
/**
|
|
* Dealer Claim Approval Service
|
|
*
|
|
* Dedicated approval service for dealer claim workflows (CLAIM_MANAGEMENT).
|
|
* Handles dealer claim-specific logic including:
|
|
* - Dynamic approver support (additional approvers added between steps)
|
|
* - Activity Creation processing
|
|
* - Dealer-specific notifications
|
|
*
|
|
* This service is separate from ApprovalService to prevent conflicts with custom workflows.
|
|
*/
|
|
|
|
import { ApprovalLevel } from '@models/ApprovalLevel';
|
|
import { WorkflowRequest } from '@models/WorkflowRequest';
|
|
import { ApprovalAction } from '../types/approval.types';
|
|
import { ApprovalStatus, WorkflowStatus } from '../types/common.types';
|
|
import { calculateTATPercentage } from '@utils/helpers';
|
|
import { calculateElapsedWorkingHours } from '@utils/tatTimeUtils';
|
|
import logger from '@utils/logger';
|
|
import { Op } from 'sequelize';
|
|
import { notificationService } from './notification.service';
|
|
import { activityService } from './activity.service';
|
|
import { tatSchedulerService } from './tatScheduler.service';
|
|
import { DealerClaimService } from './dealerClaim.service';
|
|
import { emitToRequestRoom } from '../realtime/socket';
|
|
|
|
export class DealerClaimApprovalService {
|
|
/**
|
|
* Approve a level in a dealer claim workflow
|
|
* Handles dealer claim-specific logic including dynamic approvers and activity creation
|
|
*/
|
|
async approveLevel(
|
|
levelId: string,
|
|
action: ApprovalAction,
|
|
userId: string,
|
|
requestMetadata?: { ipAddress?: string | null; userAgent?: string | null }
|
|
): Promise<ApprovalLevel | null> {
|
|
try {
|
|
const level = await ApprovalLevel.findByPk(levelId);
|
|
if (!level) return null;
|
|
|
|
// Get workflow to determine priority for working hours calculation
|
|
const wf = await WorkflowRequest.findByPk(level.requestId);
|
|
if (!wf) return null;
|
|
|
|
// Verify this is a claim management workflow
|
|
const workflowType = (wf as any)?.workflowType;
|
|
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
|
logger.warn(`[DealerClaimApproval] Attempted to use DealerClaimApprovalService for non-claim-management workflow ${level.requestId}. Workflow type: ${workflowType}`);
|
|
throw new Error('DealerClaimApprovalService can only be used for CLAIM_MANAGEMENT workflows');
|
|
}
|
|
|
|
const priority = ((wf as any)?.priority || 'standard').toString().toLowerCase();
|
|
const isPaused = (wf as any).isPaused || (level as any).isPaused;
|
|
|
|
// If paused, resume automatically when approving/rejecting
|
|
if (isPaused) {
|
|
const { pauseService } = await import('./pause.service');
|
|
try {
|
|
await pauseService.resumeWorkflow(level.requestId, userId);
|
|
logger.info(`[DealerClaimApproval] Auto-resumed paused workflow ${level.requestId} when ${action.action === 'APPROVE' ? 'approving' : 'rejecting'}`);
|
|
} catch (pauseError) {
|
|
logger.warn(`[DealerClaimApproval] Failed to auto-resume paused workflow:`, pauseError);
|
|
// Continue with approval/rejection even if resume fails
|
|
}
|
|
}
|
|
|
|
const now = new Date();
|
|
|
|
// Calculate elapsed hours using working hours logic (with pause handling)
|
|
const isPausedLevel = (level as any).isPaused;
|
|
const wasResumed = !isPausedLevel &&
|
|
(level as any).pauseElapsedHours !== null &&
|
|
(level as any).pauseElapsedHours !== undefined &&
|
|
(level as any).pauseResumeDate !== null;
|
|
|
|
const pauseInfo = isPausedLevel ? {
|
|
// Level is currently paused - return frozen elapsed hours at pause time
|
|
isPaused: true,
|
|
pausedAt: (level as any).pausedAt,
|
|
pauseElapsedHours: (level as any).pauseElapsedHours,
|
|
pauseResumeDate: (level as any).pauseResumeDate
|
|
} : wasResumed ? {
|
|
// Level was paused but has been resumed - add pre-pause elapsed hours + time since resume
|
|
isPaused: false,
|
|
pausedAt: null,
|
|
pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours
|
|
pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp
|
|
} : undefined;
|
|
|
|
const elapsedHours = await calculateElapsedWorkingHours(
|
|
(level as any).levelStartTime || (level as any).tatStartTime || now,
|
|
now,
|
|
priority,
|
|
pauseInfo
|
|
);
|
|
const tatPercentage = calculateTATPercentage(elapsedHours, level.tatHours);
|
|
|
|
// Handle rejection
|
|
if (action.action === 'REJECT') {
|
|
return await this.handleRejection(level, action, userId, requestMetadata, elapsedHours, tatPercentage, now);
|
|
}
|
|
|
|
// Update level status and elapsed time for approval
|
|
await level.update({
|
|
status: ApprovalStatus.APPROVED,
|
|
actionDate: now,
|
|
levelEndTime: now,
|
|
elapsedHours: elapsedHours,
|
|
tatPercentageUsed: tatPercentage,
|
|
comments: action.comments || undefined
|
|
});
|
|
|
|
// Check if this is the final approver
|
|
const allLevels = await ApprovalLevel.findAll({
|
|
where: { requestId: level.requestId }
|
|
});
|
|
const approvedCount = allLevels.filter((l: any) => l.status === ApprovalStatus.APPROVED).length;
|
|
const isFinalApprover = approvedCount === allLevels.length;
|
|
|
|
if (isFinalApprover) {
|
|
// Final approval - close workflow
|
|
await WorkflowRequest.update(
|
|
{
|
|
status: WorkflowStatus.APPROVED,
|
|
closureDate: now,
|
|
currentLevel: level.levelNumber || 0
|
|
},
|
|
{ where: { requestId: level.requestId } }
|
|
);
|
|
|
|
// Notify all participants
|
|
const participants = await import('@models/Participant').then(m => m.Participant.findAll({
|
|
where: { requestId: level.requestId, isActive: true }
|
|
}));
|
|
|
|
if (participants && participants.length > 0) {
|
|
const participantIds = participants.map((p: any) => p.userId).filter(Boolean);
|
|
await notificationService.sendToUsers(participantIds, {
|
|
title: `Request Approved: ${(wf as any).requestNumber}`,
|
|
body: `${(wf as any).title}`,
|
|
requestNumber: (wf as any).requestNumber,
|
|
requestId: level.requestId,
|
|
url: `/request/${(wf as any).requestNumber}`,
|
|
type: 'approval',
|
|
priority: 'MEDIUM'
|
|
});
|
|
logger.info(`[DealerClaimApproval] Final approval complete. ${participants.length} participant(s) notified.`);
|
|
}
|
|
} else {
|
|
// Not final - move to next level
|
|
// Check if workflow is paused - if so, don't advance
|
|
if ((wf as any).isPaused || (wf as any).status === 'PAUSED') {
|
|
logger.warn(`[DealerClaimApproval] Cannot advance workflow ${level.requestId} - workflow is paused`);
|
|
throw new Error('Cannot advance workflow - workflow is currently paused. Please resume the workflow first.');
|
|
}
|
|
|
|
// Find the next PENDING level (supports dynamically added approvers)
|
|
// Strategy: First try sequential, then find next PENDING level if sequential doesn't exist
|
|
const currentLevelNumber = level.levelNumber || 0;
|
|
logger.info(`[DealerClaimApproval] Finding next level after level ${currentLevelNumber} for request ${level.requestId}`);
|
|
|
|
// First, try sequential approach
|
|
let nextLevel = await ApprovalLevel.findOne({
|
|
where: {
|
|
requestId: level.requestId,
|
|
levelNumber: currentLevelNumber + 1
|
|
}
|
|
});
|
|
|
|
// If sequential level doesn't exist, search for next PENDING level
|
|
// This handles cases where additional approvers are added dynamically between steps
|
|
if (!nextLevel) {
|
|
logger.info(`[DealerClaimApproval] Sequential level ${currentLevelNumber + 1} not found, searching for next PENDING level (dynamic approvers)`);
|
|
nextLevel = await ApprovalLevel.findOne({
|
|
where: {
|
|
requestId: level.requestId,
|
|
levelNumber: { [Op.gt]: currentLevelNumber },
|
|
status: ApprovalStatus.PENDING
|
|
},
|
|
order: [['levelNumber', 'ASC']]
|
|
});
|
|
|
|
if (nextLevel) {
|
|
logger.info(`[DealerClaimApproval] Using fallback level ${nextLevel.levelNumber} (${(nextLevel as any).levelName || 'unnamed'})`);
|
|
}
|
|
} else if (nextLevel.status !== ApprovalStatus.PENDING) {
|
|
// Sequential level exists but not PENDING - check if it's already approved/rejected
|
|
if (nextLevel.status === ApprovalStatus.APPROVED || nextLevel.status === ApprovalStatus.REJECTED) {
|
|
logger.warn(`[DealerClaimApproval] Sequential level ${currentLevelNumber + 1} already ${nextLevel.status}. Skipping activation.`);
|
|
nextLevel = null; // Don't activate an already completed level
|
|
} else {
|
|
// Level exists but in unexpected status - log warning but proceed
|
|
logger.warn(`[DealerClaimApproval] Sequential level ${currentLevelNumber + 1} exists but status is ${nextLevel.status}, expected PENDING. Proceeding with sequential level.`);
|
|
}
|
|
}
|
|
|
|
const nextLevelNumber = nextLevel ? (nextLevel.levelNumber || 0) : null;
|
|
|
|
if (nextLevel) {
|
|
logger.info(`[DealerClaimApproval] Found next level: ${nextLevelNumber} (${(nextLevel as any).levelName || 'unnamed'}), approver: ${(nextLevel as any).approverName || (nextLevel as any).approverEmail || 'unknown'}, status: ${nextLevel.status}`);
|
|
} else {
|
|
logger.info(`[DealerClaimApproval] No next level found after level ${currentLevelNumber} - this may be the final approval`);
|
|
}
|
|
|
|
if (nextLevel) {
|
|
// Check if next level is paused - if so, don't activate it
|
|
if ((nextLevel as any).isPaused || (nextLevel as any).status === 'PAUSED') {
|
|
logger.warn(`[DealerClaimApproval] Cannot activate next level ${nextLevelNumber} - level is paused`);
|
|
throw new Error('Cannot activate next level - the next approval level is currently paused. Please resume it first.');
|
|
}
|
|
|
|
// Activate next level
|
|
await nextLevel.update({
|
|
status: ApprovalStatus.IN_PROGRESS,
|
|
levelStartTime: now,
|
|
tatStartTime: now
|
|
});
|
|
|
|
// Schedule TAT jobs for the next level
|
|
try {
|
|
const workflowPriority = (wf as any)?.priority || 'STANDARD';
|
|
|
|
await tatSchedulerService.scheduleTatJobs(
|
|
level.requestId,
|
|
(nextLevel as any).levelId,
|
|
(nextLevel as any).approverId,
|
|
Number((nextLevel as any).tatHours),
|
|
now,
|
|
workflowPriority
|
|
);
|
|
logger.info(`[DealerClaimApproval] TAT jobs scheduled for next level ${nextLevelNumber} (Priority: ${workflowPriority})`);
|
|
} catch (tatError) {
|
|
logger.error(`[DealerClaimApproval] Failed to schedule TAT jobs for next level:`, tatError);
|
|
// Don't fail the approval if TAT scheduling fails
|
|
}
|
|
|
|
// Update workflow current level
|
|
if (nextLevelNumber !== null) {
|
|
await WorkflowRequest.update(
|
|
{ currentLevel: nextLevelNumber },
|
|
{ where: { requestId: level.requestId } }
|
|
);
|
|
logger.info(`[DealerClaimApproval] Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
|
}
|
|
|
|
// Handle dealer claim-specific step processing
|
|
const currentLevelName = (level.levelName || '').toLowerCase();
|
|
const isDeptLeadApproval = currentLevelName.includes('department lead') || level.levelNumber === 3;
|
|
const isRequestorClaimApproval = currentLevelName.includes('requestor') &&
|
|
(currentLevelName.includes('claim') || currentLevelName.includes('approval')) ||
|
|
level.levelNumber === 5;
|
|
|
|
if (isDeptLeadApproval) {
|
|
// Activity Creation is now an activity log only - process it automatically
|
|
logger.info(`[DealerClaimApproval] Department Lead approved. Processing Activity Creation as activity log.`);
|
|
try {
|
|
const dealerClaimService = new DealerClaimService();
|
|
await dealerClaimService.processActivityCreation(level.requestId);
|
|
logger.info(`[DealerClaimApproval] Activity Creation activity logged for request ${level.requestId}`);
|
|
} catch (activityError) {
|
|
logger.error(`[DealerClaimApproval] Error processing Activity Creation activity for request ${level.requestId}:`, activityError);
|
|
// Don't fail the Department Lead approval if Activity Creation logging fails
|
|
}
|
|
} else if (isRequestorClaimApproval) {
|
|
// E-Invoice Generation is now an activity log only - will be logged when invoice is generated via DMS webhook
|
|
logger.info(`[DealerClaimApproval] Requestor Claim Approval approved. E-Invoice generation will be logged as activity when DMS webhook is received.`);
|
|
}
|
|
|
|
// Log approval activity
|
|
activityService.log({
|
|
requestId: level.requestId,
|
|
type: 'approval',
|
|
user: { userId: level.approverId, name: level.approverName },
|
|
timestamp: new Date().toISOString(),
|
|
action: 'Approved',
|
|
details: `Request approved and forwarded to ${(nextLevel as any).approverName || (nextLevel as any).approverEmail} by ${level.approverName || level.approverEmail}`,
|
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
|
userAgent: requestMetadata?.userAgent || undefined
|
|
});
|
|
|
|
// Notify initiator about the approval
|
|
if (wf) {
|
|
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
|
title: `Request Approved - Level ${level.levelNumber}`,
|
|
body: `Your request "${(wf as any).title}" has been approved by ${level.approverName || level.approverEmail} and forwarded to the next approver.`,
|
|
requestNumber: (wf as any).requestNumber,
|
|
requestId: level.requestId,
|
|
url: `/request/${(wf as any).requestNumber}`,
|
|
type: 'approval',
|
|
priority: 'MEDIUM'
|
|
});
|
|
}
|
|
|
|
// Notify next approver - ALWAYS send notification when there's a next level
|
|
if (wf && nextLevel) {
|
|
const nextApproverId = (nextLevel as any).approverId;
|
|
const nextApproverEmail = (nextLevel as any).approverEmail || '';
|
|
const nextApproverName = (nextLevel as any).approverName || nextApproverEmail || 'approver';
|
|
|
|
// Check if it's an auto-step or system process
|
|
const isAutoStep = nextApproverEmail === 'system@royalenfield.com'
|
|
|| (nextLevel as any).approverName === 'System Auto-Process'
|
|
|| nextApproverId === 'system';
|
|
|
|
const isSystemEmail = nextApproverEmail.toLowerCase() === 'system@royalenfield.com'
|
|
|| nextApproverEmail.toLowerCase().includes('system');
|
|
const isSystemName = nextApproverName.toLowerCase() === 'system auto-process'
|
|
|| nextApproverName.toLowerCase().includes('system');
|
|
|
|
// Only send notifications to real users, NOT system processes
|
|
if (!isAutoStep && !isSystemEmail && !isSystemName && nextApproverId && nextApproverId !== 'system') {
|
|
try {
|
|
logger.info(`[DealerClaimApproval] Sending assignment notification to next approver: ${nextApproverName} (${nextApproverId}) at level ${nextLevelNumber} for request ${(wf as any).requestNumber}`);
|
|
|
|
await notificationService.sendToUsers([ nextApproverId ], {
|
|
title: `Action required: ${(wf as any).requestNumber}`,
|
|
body: `${(wf as any).title}`,
|
|
requestNumber: (wf as any).requestNumber,
|
|
requestId: (wf as any).requestId,
|
|
url: `/request/${(wf as any).requestNumber}`,
|
|
type: 'assignment',
|
|
priority: 'HIGH',
|
|
actionRequired: true
|
|
});
|
|
|
|
logger.info(`[DealerClaimApproval] ✅ Assignment notification sent successfully to ${nextApproverName} (${nextApproverId}) for level ${nextLevelNumber}`);
|
|
|
|
// Log assignment activity for the next approver
|
|
await activityService.log({
|
|
requestId: level.requestId,
|
|
type: 'assignment',
|
|
user: { userId: level.approverId, name: level.approverName },
|
|
timestamp: new Date().toISOString(),
|
|
action: 'Assigned to approver',
|
|
details: `Request assigned to ${nextApproverName} for ${(nextLevel as any).levelName || `level ${nextLevelNumber}`}`,
|
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
|
userAgent: requestMetadata?.userAgent || undefined
|
|
});
|
|
} catch (notifError) {
|
|
logger.error(`[DealerClaimApproval] ❌ Failed to send notification to next approver ${nextApproverId} at level ${nextLevelNumber}:`, notifError);
|
|
// Don't throw - continue with workflow even if notification fails
|
|
}
|
|
} else {
|
|
logger.info(`[DealerClaimApproval] ⚠️ Skipping notification for system/auto-step: ${nextApproverEmail} (${nextApproverId}) at level ${nextLevelNumber}`);
|
|
}
|
|
|
|
// Notify initiator when dealer submits documents (Dealer Proposal or Dealer Completion Documents)
|
|
const levelName = (level.levelName || '').toLowerCase();
|
|
const isDealerProposalApproval = levelName.includes('dealer') && levelName.includes('proposal') || level.levelNumber === 1;
|
|
const isDealerCompletionApproval = levelName.includes('dealer') && (levelName.includes('completion') || levelName.includes('documents')) || level.levelNumber === 5;
|
|
|
|
if ((isDealerProposalApproval || isDealerCompletionApproval) && (wf as any).initiatorId) {
|
|
const stepMessage = isDealerProposalApproval
|
|
? 'Dealer proposal has been submitted and is now under review.'
|
|
: 'Dealer completion documents have been submitted and are now under review.';
|
|
|
|
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
|
title: isDealerProposalApproval ? 'Proposal Submitted' : 'Completion Documents Submitted',
|
|
body: `Your claim request "${(wf as any).title}" - ${stepMessage}`,
|
|
requestNumber: (wf as any).requestNumber,
|
|
requestId: (wf as any).requestId,
|
|
url: `/request/${(wf as any).requestNumber}`,
|
|
type: 'approval',
|
|
priority: 'MEDIUM',
|
|
actionRequired: false
|
|
});
|
|
|
|
logger.info(`[DealerClaimApproval] Sent notification to initiator for ${isDealerProposalApproval ? 'Dealer Proposal Submission' : 'Dealer Completion Documents'}`);
|
|
}
|
|
}
|
|
} else {
|
|
// No next level found but not final approver - this shouldn't happen
|
|
logger.warn(`[DealerClaimApproval] No next level found for workflow ${level.requestId} after approving level ${level.levelNumber}`);
|
|
await WorkflowRequest.update(
|
|
{
|
|
status: WorkflowStatus.APPROVED,
|
|
closureDate: now,
|
|
currentLevel: level.levelNumber || 0
|
|
},
|
|
{ where: { requestId: level.requestId } }
|
|
);
|
|
if (wf) {
|
|
await notificationService.sendToUsers([ (wf as any).initiatorId ], {
|
|
title: `Approved: ${(wf as any).requestNumber}`,
|
|
body: `${(wf as any).title}`,
|
|
requestNumber: (wf as any).requestNumber,
|
|
requestId: level.requestId,
|
|
url: `/request/${(wf as any).requestNumber}`,
|
|
type: 'approval',
|
|
priority: 'MEDIUM'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emit real-time update to all users viewing this request
|
|
emitToRequestRoom(level.requestId, 'request:updated', {
|
|
requestId: level.requestId,
|
|
requestNumber: (wf as any)?.requestNumber,
|
|
action: action.action,
|
|
levelNumber: level.levelNumber,
|
|
timestamp: now.toISOString()
|
|
});
|
|
|
|
logger.info(`[DealerClaimApproval] Approval level ${levelId} ${action.action.toLowerCase()}ed and socket event emitted`);
|
|
|
|
return level;
|
|
} catch (error) {
|
|
logger.error('[DealerClaimApproval] Error approving level:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle rejection (internal method called from approveLevel)
|
|
*/
|
|
private async handleRejection(
|
|
level: ApprovalLevel,
|
|
action: ApprovalAction,
|
|
userId: string,
|
|
requestMetadata?: { ipAddress?: string | null; userAgent?: string | null },
|
|
elapsedHours?: number,
|
|
tatPercentage?: number,
|
|
now?: Date
|
|
): Promise<ApprovalLevel | null> {
|
|
const rejectionNow = now || new Date();
|
|
const wf = await WorkflowRequest.findByPk(level.requestId);
|
|
if (!wf) return null;
|
|
|
|
// Update level status
|
|
await level.update({
|
|
status: ApprovalStatus.REJECTED,
|
|
actionDate: rejectionNow,
|
|
levelEndTime: rejectionNow,
|
|
elapsedHours: elapsedHours || 0,
|
|
tatPercentageUsed: tatPercentage || 0,
|
|
comments: action.comments || action.rejectionReason || undefined
|
|
});
|
|
|
|
// Close workflow
|
|
await WorkflowRequest.update(
|
|
{
|
|
status: WorkflowStatus.REJECTED,
|
|
closureDate: rejectionNow
|
|
},
|
|
{ where: { requestId: level.requestId } }
|
|
);
|
|
|
|
// Log rejection activity
|
|
activityService.log({
|
|
requestId: level.requestId,
|
|
type: 'rejection',
|
|
user: { userId: level.approverId, name: level.approverName },
|
|
timestamp: new Date().toISOString(),
|
|
action: 'Rejected',
|
|
details: `Request rejected by ${level.approverName || level.approverEmail}. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
|
userAgent: requestMetadata?.userAgent || undefined
|
|
});
|
|
|
|
// Notify initiator and participants
|
|
const participants = await import('@models/Participant').then(m => m.Participant.findAll({
|
|
where: { requestId: level.requestId, isActive: true }
|
|
}));
|
|
|
|
const userIdsToNotify = [(wf as any).initiatorId];
|
|
if (participants && participants.length > 0) {
|
|
participants.forEach((p: any) => {
|
|
if (p.userId && p.userId !== (wf as any).initiatorId) {
|
|
userIdsToNotify.push(p.userId);
|
|
}
|
|
});
|
|
}
|
|
|
|
await notificationService.sendToUsers(userIdsToNotify, {
|
|
title: `Request Rejected: ${(wf as any).requestNumber}`,
|
|
body: `${(wf as any).title} - Rejected by ${level.approverName || level.approverEmail}. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
|
requestNumber: (wf as any).requestNumber,
|
|
requestId: level.requestId,
|
|
url: `/request/${(wf as any).requestNumber}`,
|
|
type: 'rejection',
|
|
priority: 'HIGH'
|
|
});
|
|
|
|
// Emit real-time update to all users viewing this request
|
|
emitToRequestRoom(level.requestId, 'request:updated', {
|
|
requestId: level.requestId,
|
|
requestNumber: (wf as any)?.requestNumber,
|
|
action: 'REJECT',
|
|
levelNumber: level.levelNumber,
|
|
timestamp: rejectionNow.toISOString()
|
|
});
|
|
|
|
return level;
|
|
}
|
|
|
|
/**
|
|
* Reject a level in a dealer claim workflow (legacy method - kept for backward compatibility)
|
|
*/
|
|
async rejectLevel(
|
|
levelId: string,
|
|
reason: string,
|
|
comments: string,
|
|
userId: string,
|
|
requestMetadata?: { ipAddress?: string | null; userAgent?: string | null }
|
|
): Promise<ApprovalLevel | null> {
|
|
try {
|
|
const level = await ApprovalLevel.findByPk(levelId);
|
|
if (!level) return null;
|
|
|
|
const wf = await WorkflowRequest.findByPk(level.requestId);
|
|
if (!wf) return null;
|
|
|
|
// Verify this is a claim management workflow
|
|
const workflowType = (wf as any)?.workflowType;
|
|
if (workflowType !== 'CLAIM_MANAGEMENT') {
|
|
logger.warn(`[DealerClaimApproval] Attempted to use DealerClaimApprovalService for non-claim-management workflow ${level.requestId}. Workflow type: ${workflowType}`);
|
|
throw new Error('DealerClaimApprovalService can only be used for CLAIM_MANAGEMENT workflows');
|
|
}
|
|
|
|
const now = new Date();
|
|
|
|
// Calculate elapsed hours
|
|
const priority = ((wf as any)?.priority || 'standard').toString().toLowerCase();
|
|
const isPausedLevel = (level as any).isPaused;
|
|
const wasResumed = !isPausedLevel &&
|
|
(level as any).pauseElapsedHours !== null &&
|
|
(level as any).pauseElapsedHours !== undefined &&
|
|
(level as any).pauseResumeDate !== null;
|
|
|
|
const pauseInfo = isPausedLevel ? {
|
|
// Level is currently paused - return frozen elapsed hours at pause time
|
|
isPaused: true,
|
|
pausedAt: (level as any).pausedAt,
|
|
pauseElapsedHours: (level as any).pauseElapsedHours,
|
|
pauseResumeDate: (level as any).pauseResumeDate
|
|
} : wasResumed ? {
|
|
// Level was paused but has been resumed - add pre-pause elapsed hours + time since resume
|
|
isPaused: false,
|
|
pausedAt: null,
|
|
pauseElapsedHours: Number((level as any).pauseElapsedHours), // Pre-pause elapsed hours
|
|
pauseResumeDate: (level as any).pauseResumeDate // Actual resume timestamp
|
|
} : undefined;
|
|
|
|
// Use the internal handleRejection method
|
|
const elapsedHours = await calculateElapsedWorkingHours(
|
|
(level as any).levelStartTime || (level as any).tatStartTime || now,
|
|
now,
|
|
priority,
|
|
pauseInfo
|
|
);
|
|
const tatPercentage = calculateTATPercentage(elapsedHours, level.tatHours);
|
|
|
|
return await this.handleRejection(
|
|
level,
|
|
{ action: 'REJECT', comments: comments || reason, rejectionReason: reason || comments },
|
|
userId,
|
|
requestMetadata,
|
|
elapsedHours,
|
|
tatPercentage,
|
|
now
|
|
);
|
|
} catch (error) {
|
|
logger.error('[DealerClaimApproval] Error rejecting level:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current approval level for a request
|
|
*/
|
|
async getCurrentApprovalLevel(requestId: string): Promise<ApprovalLevel | null> {
|
|
const workflow = await WorkflowRequest.findByPk(requestId);
|
|
if (!workflow) return null;
|
|
|
|
const currentLevel = (workflow as any).currentLevel;
|
|
if (!currentLevel) return null;
|
|
|
|
return await ApprovalLevel.findOne({
|
|
where: { requestId, levelNumber: currentLevel }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all approval levels for a request
|
|
*/
|
|
async getApprovalLevels(requestId: string): Promise<ApprovalLevel[]> {
|
|
return await ApprovalLevel.findAll({
|
|
where: { requestId },
|
|
order: [['levelNumber', 'ASC']]
|
|
});
|
|
}
|
|
}
|
|
|