Re_Backend/src/services/approval.service.ts

269 lines
11 KiB
TypeScript

import { ApprovalLevel } from '@models/ApprovalLevel';
import { WorkflowRequest } from '@models/WorkflowRequest';
import { Participant } from '@models/Participant';
import { TatAlert } from '@models/TatAlert';
import { ApprovalAction } from '../types/approval.types';
import { ApprovalStatus, WorkflowStatus } from '../types/common.types';
import { calculateElapsedHours, calculateTATPercentage } from '@utils/helpers';
import logger from '@utils/logger';
import { Op } from 'sequelize';
import { notificationService } from './notification.service';
import { activityService } from './activity.service';
import { tatSchedulerService } from './tatScheduler.service';
export class ApprovalService {
async approveLevel(levelId: string, action: ApprovalAction, _userId: string): Promise<ApprovalLevel | null> {
try {
const level = await ApprovalLevel.findByPk(levelId);
if (!level) return null;
const now = new Date();
const elapsedHours = calculateElapsedHours(level.levelStartTime || level.createdAt, now);
const tatPercentage = calculateTATPercentage(elapsedHours, level.tatHours);
const updateData = {
status: action.action === 'APPROVE' ? ApprovalStatus.APPROVED : ApprovalStatus.REJECTED,
actionDate: now,
levelEndTime: now,
elapsedHours,
tatPercentageUsed: tatPercentage,
comments: action.comments,
rejectionReason: action.rejectionReason
};
const updatedLevel = await level.update(updateData);
// Cancel TAT jobs for the current level since it's been actioned
try {
await tatSchedulerService.cancelTatJobs(level.requestId, level.levelId);
logger.info(`[Approval] TAT jobs cancelled for level ${level.levelId}`);
} catch (tatError) {
logger.error(`[Approval] Failed to cancel TAT jobs:`, tatError);
// Don't fail the approval if TAT cancellation fails
}
// Update TAT alerts for this level to mark completion status
try {
const wasOnTime = elapsedHours <= level.tatHours;
await TatAlert.update(
{
wasCompletedOnTime: wasOnTime,
completionTime: now
},
{
where: { levelId: level.levelId }
}
);
logger.info(`[Approval] TAT alerts updated for level ${level.levelId} - Completed ${wasOnTime ? 'on time' : 'late'}`);
} catch (tatAlertError) {
logger.error(`[Approval] Failed to update TAT alerts:`, tatAlertError);
// Don't fail the approval if TAT alert update fails
}
// Load workflow for titles and initiator
const wf = await WorkflowRequest.findByPk(level.requestId);
// Handle approval - move to next level or close workflow
if (action.action === 'APPROVE') {
if (level.isFinalApprover) {
// Final approver - close workflow as APPROVED
await WorkflowRequest.update(
{
status: WorkflowStatus.APPROVED,
closureDate: now,
currentLevel: (level.levelNumber || 0) + 1
},
{ where: { requestId: level.requestId } }
);
logger.info(`Final approver approved. Workflow ${level.requestId} closed as APPROVED`);
// Notify initiator
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,
url: `/request/${(wf as any).requestNumber}`
});
activityService.log({
requestId: level.requestId,
type: 'approval',
user: { userId: level.approverId, name: level.approverName },
timestamp: new Date().toISOString(),
action: 'Approved',
details: `Request approved and finalized by ${level.approverName || level.approverEmail}`
});
}
} else {
// Not final - move to next level
const nextLevelNumber = (level.levelNumber || 0) + 1;
const nextLevel = await ApprovalLevel.findOne({
where: {
requestId: level.requestId,
levelNumber: nextLevelNumber
}
});
if (nextLevel) {
// Activate next level
await nextLevel.update({
status: ApprovalStatus.IN_PROGRESS,
levelStartTime: now,
tatStartTime: now
});
// Schedule TAT jobs for the next level
try {
// Get workflow priority for TAT calculation
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 // Pass workflow priority (EXPRESS = 24/7, STANDARD = working hours)
);
logger.info(`[Approval] TAT jobs scheduled for next level ${nextLevelNumber} (Priority: ${workflowPriority})`);
} catch (tatError) {
logger.error(`[Approval] Failed to schedule TAT jobs for next level:`, tatError);
// Don't fail the approval if TAT scheduling fails
}
// Update workflow current level
await WorkflowRequest.update(
{ currentLevel: nextLevelNumber },
{ where: { requestId: level.requestId } }
);
logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
// Notify next approver
if (wf && nextLevel) {
await notificationService.sendToUsers([ (nextLevel as any).approverId ], {
title: `Action required: ${(wf as any).requestNumber}`,
body: `${(wf as any).title}`,
requestNumber: (wf as any).requestNumber,
url: `/request/${(wf as any).requestNumber}`
});
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}`
});
}
} else {
// No next level found but not final approver - this shouldn't happen
logger.warn(`No next level found for workflow ${level.requestId} after approving level ${level.levelNumber}`);
await WorkflowRequest.update(
{
status: WorkflowStatus.APPROVED,
closureDate: now,
currentLevel: nextLevelNumber
},
{ 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,
url: `/request/${(wf as any).requestNumber}`
});
activityService.log({
requestId: level.requestId,
type: 'approval',
user: { userId: level.approverId, name: level.approverName },
timestamp: new Date().toISOString(),
action: 'Approved',
details: `Request approved and finalized by ${level.approverName || level.approverEmail}`
});
}
}
}
} else if (action.action === 'REJECT') {
// Rejection - close workflow and mark all remaining levels as skipped
await WorkflowRequest.update(
{
status: WorkflowStatus.REJECTED,
closureDate: now
},
{ where: { requestId: level.requestId } }
);
// Mark all pending levels as skipped
await ApprovalLevel.update(
{
status: ApprovalStatus.SKIPPED,
levelEndTime: now
},
{
where: {
requestId: level.requestId,
status: ApprovalStatus.PENDING,
levelNumber: { [Op.gt]: level.levelNumber }
}
}
);
logger.info(`Level ${level.levelNumber} rejected. Workflow ${level.requestId} closed as REJECTED`);
// Notify initiator and all participants
if (wf) {
const participants = await Participant.findAll({ where: { requestId: level.requestId } });
const targetUserIds = new Set<string>();
targetUserIds.add((wf as any).initiatorId);
for (const p of participants as any[]) {
targetUserIds.add(p.userId);
}
await notificationService.sendToUsers(Array.from(targetUserIds), {
title: `Rejected: ${(wf as any).requestNumber}`,
body: `${(wf as any).title}`,
requestNumber: (wf as any).requestNumber,
url: `/request/${(wf as any).requestNumber}`
});
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'}`
});
}
}
logger.info(`Approval level ${levelId} ${action.action.toLowerCase()}ed`);
return updatedLevel;
} catch (error) {
logger.error(`Failed to ${action.action.toLowerCase()} level ${levelId}:`, error);
throw new Error(`Failed to ${action.action.toLowerCase()} level`);
}
}
async getCurrentApprovalLevel(requestId: string): Promise<ApprovalLevel | null> {
try {
return await ApprovalLevel.findOne({
where: { requestId, status: ApprovalStatus.PENDING },
order: [['levelNumber', 'ASC']]
});
} catch (error) {
logger.error(`Failed to get current approval level for ${requestId}:`, error);
throw new Error('Failed to get current approval level');
}
}
async getApprovalLevels(requestId: string): Promise<ApprovalLevel[]> {
try {
return await ApprovalLevel.findAll({
where: { requestId },
order: [['levelNumber', 'ASC']]
});
} catch (error) {
logger.error(`Failed to get approval levels for ${requestId}:`, error);
throw new Error('Failed to get approval levels');
}
}
}