Re_Backend/_archive/services/pause.service.ts

765 lines
29 KiB
TypeScript

import { WorkflowRequest } from '@models/WorkflowRequest';
import { ApprovalLevel } from '@models/ApprovalLevel';
import { User } from '@models/User';
import { ApprovalStatus, WorkflowStatus } from '../types/common.types';
import { Op } from 'sequelize';
import logger from '@utils/logger';
import { tatSchedulerService } from './tatScheduler.service';
import { calculateElapsedWorkingHours } from '@utils/tatTimeUtils';
import { notificationService } from './notification.service';
import { activityService } from './activity.service';
import dayjs from 'dayjs';
import { emitToRequestRoom } from '../realtime/socket';
export class PauseService {
/**
* Pause a workflow at a specific approval level
* @param requestId - The workflow request ID
* @param levelId - The approval level ID to pause (optional, pauses current level if not provided)
* @param userId - The user ID who is pausing
* @param reason - Reason for pausing
* @param resumeDate - Date when workflow should auto-resume (max 1 month from now)
*/
async pauseWorkflow(
requestId: string,
levelId: string | null,
userId: string,
reason: string,
resumeDate: Date
): Promise<{ workflow: WorkflowRequest; level: ApprovalLevel | null }> {
try {
// Validate resume date (max 1 month from now)
const now = new Date();
const maxResumeDate = dayjs(now).add(1, 'month').toDate();
if (resumeDate > maxResumeDate) {
throw new Error('Resume date cannot be more than 1 month from now');
}
if (resumeDate <= now) {
throw new Error('Resume date must be in the future');
}
// Get workflow
const workflow = await WorkflowRequest.findByPk(requestId);
if (!workflow) {
throw new Error('Workflow not found');
}
// Check if already paused
if ((workflow as any).isPaused) {
throw new Error('Workflow is already paused');
}
// Get current approval level
let level: ApprovalLevel | null = null;
if (levelId) {
level = await ApprovalLevel.findByPk(levelId);
if (!level || (level as any).requestId !== requestId) {
throw new Error('Approval level not found or does not belong to this workflow');
}
} else {
// Get current active level
level = await ApprovalLevel.findOne({
where: {
requestId,
status: { [Op.in]: [ApprovalStatus.PENDING, ApprovalStatus.IN_PROGRESS] }
},
order: [['levelNumber', 'ASC']]
});
}
if (!level) {
throw new Error('No active approval level found to pause');
}
// Verify user is either the approver for this level OR the initiator
const isApprover = (level as any).approverId === userId;
const isInitiator = (workflow as any).initiatorId === userId;
if (!isApprover && !isInitiator) {
throw new Error('Only the assigned approver or the initiator can pause this workflow');
}
// Check if level is already paused
if ((level as any).isPaused) {
throw new Error('This approval level is already paused');
}
// Calculate elapsed hours before pause
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
// Check if this level was previously paused and resumed
// If so, we need to account for the previous pauseElapsedHours
// IMPORTANT: Convert to number to avoid string concatenation (DB returns DECIMAL as string)
const previousPauseElapsedHours = Number((level as any).pauseElapsedHours || 0);
const previousResumeDate = (level as any).pauseResumeDate;
const originalTatStartTime = (level as any).pauseTatStartTime || (level as any).levelStartTime || (level as any).tatStartTime || (level as any).createdAt;
let elapsedHours: number;
let levelStartTimeForCalculation: Date;
if (previousPauseElapsedHours > 0 && previousResumeDate) {
// This is a second (or subsequent) pause
// Calculate: previous elapsed hours + time from resume to now
levelStartTimeForCalculation = previousResumeDate; // Start from last resume time
const timeSinceResume = await calculateElapsedWorkingHours(levelStartTimeForCalculation, now, priority);
elapsedHours = previousPauseElapsedHours + Number(timeSinceResume);
logger.info(`[Pause] Second pause detected - Previous elapsed: ${previousPauseElapsedHours}h, Since resume: ${timeSinceResume}h, Total: ${elapsedHours}h`);
} else {
// First pause - calculate from original start time
levelStartTimeForCalculation = originalTatStartTime;
elapsedHours = await calculateElapsedWorkingHours(levelStartTimeForCalculation, now, priority);
}
// Store TAT snapshot
const tatSnapshot = {
levelId: (level as any).levelId,
levelNumber: (level as any).levelNumber,
elapsedHours: Number(elapsedHours),
remainingHours: Math.max(0, Number((level as any).tatHours) - elapsedHours),
tatPercentageUsed: (Number((level as any).tatHours) > 0
? Math.min(100, Math.round((elapsedHours / Number((level as any).tatHours)) * 100))
: 0),
pausedAt: now.toISOString(),
originalTatStartTime: originalTatStartTime // Always use the original start time, not the resume time
};
// Update approval level with pause information
await level.update({
isPaused: true,
pausedAt: now,
pausedBy: userId,
pauseReason: reason,
pauseResumeDate: resumeDate,
pauseTatStartTime: originalTatStartTime, // Always preserve the original start time
pauseElapsedHours: elapsedHours,
status: ApprovalStatus.PAUSED
});
// Update workflow with pause information
// Store the current status before pausing so we can restore it on resume
const currentWorkflowStatus = (workflow as any).status;
const currentLevel = (workflow as any).currentLevel || (level as any).levelNumber;
await workflow.update({
isPaused: true,
pausedAt: now,
pausedBy: userId,
pauseReason: reason,
pauseResumeDate: resumeDate,
pauseTatSnapshot: {
...tatSnapshot,
previousStatus: currentWorkflowStatus, // Store previous status for resume
previousCurrentLevel: currentLevel // Store current level to prevent advancement
},
status: WorkflowStatus.PAUSED
// Note: We do NOT update currentLevel here - it should stay at the paused level
});
// Cancel TAT jobs for this level
await tatSchedulerService.cancelTatJobs(requestId, (level as any).levelId);
// Get user details for notifications
const user = await User.findByPk(userId);
const userName = (user as any)?.displayName || (user as any)?.email || 'User';
// Get initiator
const initiator = await User.findByPk((workflow as any).initiatorId);
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
// Send notifications
const requestNumber = (workflow as any).requestNumber;
const title = (workflow as any).title;
// Notify initiator only if someone else (approver) paused the request
// Skip notification if initiator paused their own request
if (!isInitiator) {
await notificationService.sendToUsers([(workflow as any).initiatorId], {
title: 'Workflow Paused',
body: `Your request "${title}" has been paused by ${userName}. Reason: ${reason}. Will resume on ${dayjs(resumeDate).format('MMM DD, YYYY')}.`,
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'workflow_paused',
priority: 'HIGH',
actionRequired: false,
metadata: {
pauseReason: reason,
resumeDate: resumeDate.toISOString(),
pausedBy: userId
}
});
}
// Notify the user who paused (confirmation) - no email for self-action
await notificationService.sendToUsers([userId], {
title: 'Workflow Paused Successfully',
body: `You have paused request "${title}". It will automatically resume on ${dayjs(resumeDate).format('MMM DD, YYYY')}.`,
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'status_change', // Use status_change to avoid email for self-action
priority: 'MEDIUM',
actionRequired: false
});
// If initiator paused, notify the current approver
if (isInitiator && (level as any).approverId) {
const approver = await User.findByPk((level as any).approverId);
const approverUserId = (level as any).approverId;
await notificationService.sendToUsers([approverUserId], {
title: 'Workflow Paused by Initiator',
body: `Request "${title}" has been paused by the initiator (${userName}). Reason: ${reason}. Will resume on ${dayjs(resumeDate).format('MMM DD, YYYY')}.`,
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'workflow_paused',
priority: 'HIGH',
actionRequired: false,
metadata: {
pauseReason: reason,
resumeDate: resumeDate.toISOString(),
pausedBy: userId
}
});
}
// Log activity
await activityService.log({
requestId,
type: 'paused',
user: { userId, name: userName },
timestamp: now.toISOString(),
action: 'Workflow Paused',
details: `Workflow paused by ${userName} at level ${(level as any).levelNumber}. Reason: ${reason}. Will resume on ${dayjs(resumeDate).format('MMM DD, YYYY')}.`,
metadata: {
levelId: (level as any).levelId,
levelNumber: (level as any).levelNumber,
resumeDate: resumeDate.toISOString()
}
});
logger.info(`[Pause] Workflow ${requestId} paused at level ${(level as any).levelNumber} by ${userId}`);
// Schedule dedicated auto-resume job for this workflow
try {
const { pauseResumeQueue } = require('../queues/pauseResumeQueue');
if (pauseResumeQueue && resumeDate) {
const delay = resumeDate.getTime() - now.getTime();
if (delay > 0) {
const jobId = `resume-${requestId}-${(level as any).levelId}`;
await pauseResumeQueue.add(
'auto-resume-workflow',
{
type: 'auto-resume-workflow',
requestId,
levelId: (level as any).levelId,
scheduledResumeDate: resumeDate.toISOString()
},
{
jobId,
delay, // Exact delay in milliseconds until resume time
removeOnComplete: true,
removeOnFail: false
}
);
logger.info(`[Pause] Scheduled dedicated auto-resume job ${jobId} for ${resumeDate.toISOString()} (delay: ${Math.round(delay / 1000 / 60)} minutes)`);
} else {
logger.warn(`[Pause] Resume date ${resumeDate.toISOString()} is in the past, skipping job scheduling`);
}
}
} catch (queueError) {
logger.warn(`[Pause] Could not schedule dedicated auto-resume job:`, queueError);
// Continue with pause even if job scheduling fails (hourly check will handle it as fallback)
}
// Emit real-time update to all users viewing this request
emitToRequestRoom(requestId, 'request:updated', {
requestId,
requestNumber: (workflow as any).requestNumber,
action: 'PAUSE',
levelNumber: (level as any).levelNumber,
timestamp: now.toISOString()
});
return { workflow, level };
} catch (error: any) {
logger.error(`[Pause] Failed to pause workflow:`, error);
throw error;
}
}
/**
* Resume a paused workflow
* @param requestId - The workflow request ID
* @param userId - The user ID who is resuming (optional, for manual resume)
* @param notes - Optional notes for the resume action
*/
async resumeWorkflow(requestId: string, userId?: string, notes?: string): Promise<{ workflow: WorkflowRequest; level: ApprovalLevel | null }> {
try {
const now = new Date();
// Get workflow
const workflow = await WorkflowRequest.findByPk(requestId);
if (!workflow) {
throw new Error('Workflow not found');
}
// Check if paused
if (!(workflow as any).isPaused) {
throw new Error('Workflow is not paused');
}
// Get paused level
const level = await ApprovalLevel.findOne({
where: {
requestId,
isPaused: true
},
order: [['levelNumber', 'ASC']]
});
if (!level) {
throw new Error('Paused approval level not found');
}
// Verify user has permission (if manual resume)
// Both initiator and current approver can resume the workflow
if (userId) {
const isApprover = (level as any).approverId === userId;
const isInitiator = (workflow as any).initiatorId === userId;
if (!isApprover && !isInitiator) {
throw new Error('Only the assigned approver or the initiator can resume this workflow');
}
}
// Calculate remaining TAT from resume time
const priority = ((workflow as any).priority || 'STANDARD').toString().toLowerCase();
const pauseElapsedHours = Number((level as any).pauseElapsedHours || 0);
const tatHours = Number((level as any).tatHours);
const remainingHours = Math.max(0, tatHours - pauseElapsedHours);
// Get which alerts have already been sent (to avoid re-sending on resume)
const tat50AlertSent = (level as any).tat50AlertSent || false;
const tat75AlertSent = (level as any).tat75AlertSent || false;
const tatBreached = (level as any).tatBreached || false;
// Update approval level - resume TAT
// IMPORTANT: Keep pauseElapsedHours and store resumedAt (pauseResumeDate repurposed)
// This allows SLA calculation to correctly add pre-pause elapsed time
await level.update({
isPaused: false,
pausedAt: null as any,
pausedBy: null as any,
pauseReason: null as any,
pauseResumeDate: now, // Store actual resume time (repurposed from scheduled resume date)
// pauseTatStartTime: null as any, // Keep original TAT start time for reference
// pauseElapsedHours is intentionally NOT cleared - needed for SLA calculations
status: ApprovalStatus.IN_PROGRESS,
tatStartTime: now, // Reset TAT start time to now for new elapsed calculation
levelStartTime: now // This is the new start time from resume
});
// Cancel any scheduled auto-resume job (if exists)
try {
const { pauseResumeQueue } = require('../queues/pauseResumeQueue');
if (pauseResumeQueue) {
// Try to remove job by specific ID pattern first (more efficient)
const jobId = `resume-${requestId}-${(level as any).levelId}`;
try {
const specificJob = await pauseResumeQueue.getJob(jobId);
if (specificJob) {
await specificJob.remove();
logger.info(`[Pause] Cancelled scheduled auto-resume job ${jobId} for workflow ${requestId}`);
}
} catch (err) {
// Job might not exist, which is fine
}
// Also check for any other jobs for this request (fallback for old jobs)
const scheduledJobs = await pauseResumeQueue.getJobs(['delayed', 'waiting']);
const otherJobs = scheduledJobs.filter((job: any) =>
job.data.requestId === requestId && job.id !== jobId
);
for (const job of otherJobs) {
await job.remove();
logger.info(`[Pause] Cancelled legacy auto-resume job ${job.id} for workflow ${requestId}`);
}
}
} catch (queueError) {
logger.warn(`[Pause] Could not cancel scheduled auto-resume job:`, queueError);
// Continue with resume even if job cancellation fails
}
// Update workflow - restore previous status or default to PENDING
const pauseSnapshot = (workflow as any).pauseTatSnapshot || {};
const previousStatus = pauseSnapshot.previousStatus || WorkflowStatus.PENDING;
await workflow.update({
isPaused: false,
pausedAt: null as any,
pausedBy: null as any,
pauseReason: null as any,
pauseResumeDate: null as any,
pauseTatSnapshot: null as any,
status: previousStatus // Restore previous status (PENDING or IN_PROGRESS)
});
// Reschedule TAT jobs from resume time - only for alerts that haven't been sent yet
if (remainingHours > 0) {
// Calculate which thresholds are still pending based on remaining time
const percentageUsedAtPause = tatHours > 0 ? (pauseElapsedHours / tatHours) * 100 : 0;
// Only schedule jobs for thresholds that:
// 1. Haven't been sent yet
// 2. Haven't been passed yet (based on percentage used at pause)
await tatSchedulerService.scheduleTatJobsOnResume(
requestId,
(level as any).levelId,
(level as any).approverId,
remainingHours, // Remaining TAT hours
now, // Start from now
priority as any,
{
// Pass which alerts were already sent
tat50AlertSent: tat50AlertSent,
tat75AlertSent: tat75AlertSent,
tatBreached: tatBreached,
// Pass percentage used at pause to determine which thresholds are still relevant
percentageUsedAtPause: percentageUsedAtPause
}
);
}
// Get user details
const resumeUser = userId ? await User.findByPk(userId) : null;
const resumeUserName = resumeUser
? ((resumeUser as any)?.displayName || (resumeUser as any)?.email || 'User')
: 'System (Auto-resume)';
// Get initiator and paused by user
const initiator = await User.findByPk((workflow as any).initiatorId);
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
const pausedByUser = (workflow as any).pausedBy
? await User.findByPk((workflow as any).pausedBy)
: null;
const pausedByName = pausedByUser
? ((pausedByUser as any)?.displayName || (pausedByUser as any)?.email || 'User')
: 'Unknown';
const requestNumber = (workflow as any).requestNumber;
const title = (workflow as any).title;
const initiatorId = (workflow as any).initiatorId;
const approverId = (level as any).approverId;
const isResumedByInitiator = userId === initiatorId;
const isResumedByApprover = userId === approverId;
// Calculate pause duration
const pausedAt = (level as any).pausedAt || (workflow as any).pausedAt;
const pauseDurationMs = pausedAt ? now.getTime() - new Date(pausedAt).getTime() : 0;
const pauseDurationHours = Math.round((pauseDurationMs / (1000 * 60 * 60)) * 100) / 100; // Round to 2 decimal places
const pauseDuration = pauseDurationHours > 0 ? `${pauseDurationHours} hours` : 'less than 1 hour';
// Notify initiator only if someone else resumed (or auto-resume)
// Skip if initiator resumed their own request
if (!isResumedByInitiator) {
await notificationService.sendToUsers([initiatorId], {
title: 'Workflow Resumed',
body: `Your request "${title}" has been resumed ${userId ? `by ${resumeUserName}` : 'automatically'}.`,
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'workflow_resumed',
priority: 'HIGH',
actionRequired: false,
metadata: {
resumedBy: userId ? { userId, name: resumeUserName } : null,
pauseDuration: pauseDuration
}
});
}
// Notify approver only if someone else resumed (or auto-resume)
// Skip if approver resumed the request themselves
if (!isResumedByApprover && approverId) {
await notificationService.sendToUsers([approverId], {
title: 'Workflow Resumed',
body: `Request "${title}" has been resumed ${userId ? `by ${resumeUserName}` : 'automatically'}. Please continue with your review.`,
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'workflow_resumed',
priority: 'HIGH',
actionRequired: true,
metadata: {
resumedBy: userId ? { userId, name: resumeUserName } : null,
pauseDuration: pauseDuration
}
});
}
// Send confirmation to the user who resumed (if manual resume) - no email for self-action
if (userId) {
await notificationService.sendToUsers([userId], {
title: 'Workflow Resumed Successfully',
body: `You have resumed request "${title}". ${isResumedByApprover ? 'Please continue with your review.' : ''}`,
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'status_change', // Use status_change to avoid email for self-action
priority: 'MEDIUM',
actionRequired: isResumedByApprover
});
}
// Log activity with notes
const resumeDetails = notes
? `Workflow resumed ${userId ? `by ${resumeUserName}` : 'automatically'} at level ${(level as any).levelNumber}. Notes: ${notes}`
: `Workflow resumed ${userId ? `by ${resumeUserName}` : 'automatically'} at level ${(level as any).levelNumber}.`;
await activityService.log({
requestId,
type: 'resumed',
user: userId ? { userId, name: resumeUserName } : undefined,
timestamp: now.toISOString(),
action: 'Workflow Resumed',
details: resumeDetails,
metadata: {
levelId: (level as any).levelId,
levelNumber: (level as any).levelNumber,
wasAutoResume: !userId,
notes: notes || null
}
});
logger.info(`[Pause] Workflow ${requestId} resumed ${userId ? `by ${userId}` : 'automatically'}`);
// Emit real-time update to all users viewing this request
emitToRequestRoom(requestId, 'request:updated', {
requestId,
requestNumber: (workflow as any).requestNumber,
action: 'RESUME',
levelNumber: (level as any).levelNumber,
timestamp: now.toISOString()
});
return { workflow, level };
} catch (error: any) {
logger.error(`[Pause] Failed to resume workflow:`, error);
throw error;
}
}
/**
* Cancel pause (for retrigger scenario - initiator requests approver to resume)
* This sends a notification to the approver who paused it
* @param requestId - The workflow request ID
* @param userId - The initiator user ID
*/
async retriggerPause(requestId: string, userId: string): Promise<void> {
try {
const workflow = await WorkflowRequest.findByPk(requestId);
if (!workflow) {
throw new Error('Workflow not found');
}
if (!(workflow as any).isPaused) {
throw new Error('Workflow is not paused');
}
// Verify user is initiator
if ((workflow as any).initiatorId !== userId) {
throw new Error('Only the initiator can retrigger a pause');
}
const pausedBy = (workflow as any).pausedBy;
if (!pausedBy) {
throw new Error('Cannot retrigger - no approver found who paused this workflow');
}
// Get user details
const initiator = await User.findByPk(userId);
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
// Get approver details (who paused the workflow)
const approver = await User.findByPk(pausedBy);
const approverName = (approver as any)?.displayName || (approver as any)?.email || 'Approver';
const requestNumber = (workflow as any).requestNumber;
const title = (workflow as any).title;
// Notify approver who paused it
await notificationService.sendToUsers([pausedBy], {
title: 'Pause Retrigger Request',
body: `${initiatorName} is requesting you to cancel the pause and resume work on request "${title}".`,
requestId,
requestNumber,
url: `/request/${requestNumber}`,
type: 'pause_retrigger_request',
priority: 'HIGH',
actionRequired: true
});
// Log activity with approver name
await activityService.log({
requestId,
type: 'pause_retriggered',
user: { userId, name: initiatorName },
timestamp: new Date().toISOString(),
action: 'Pause Retrigger Requested',
details: `${initiatorName} requested ${approverName} to cancel the pause and resume work.`,
metadata: {
pausedBy,
approverName
}
});
logger.info(`[Pause] Pause retrigger requested for workflow ${requestId} by initiator ${userId}`);
} catch (error: any) {
logger.error(`[Pause] Failed to retrigger pause:`, error);
throw error;
}
}
/**
* Get pause details for a workflow
*/
async getPauseDetails(requestId: string): Promise<any> {
try {
const workflow = await WorkflowRequest.findByPk(requestId);
if (!workflow) {
throw new Error('Workflow not found');
}
if (!(workflow as any).isPaused) {
return null;
}
const level = await ApprovalLevel.findOne({
where: {
requestId,
isPaused: true
}
});
const pausedByUser = (workflow as any).pausedBy
? await User.findByPk((workflow as any).pausedBy, { attributes: ['userId', 'email', 'displayName'] })
: null;
return {
isPaused: true,
pausedAt: (workflow as any).pausedAt,
pausedBy: pausedByUser ? {
userId: (pausedByUser as any).userId,
email: (pausedByUser as any).email,
name: (pausedByUser as any).displayName || (pausedByUser as any).email
} : null,
pauseReason: (workflow as any).pauseReason,
pauseResumeDate: (workflow as any).pauseResumeDate,
level: level ? {
levelId: (level as any).levelId,
levelNumber: (level as any).levelNumber,
approverName: (level as any).approverName
} : null
};
} catch (error: any) {
logger.error(`[Pause] Failed to get pause details:`, error);
throw error;
}
}
/**
* Check and auto-resume paused workflows whose resume date has passed
* This is called by a scheduled job
*/
async checkAndResumePausedWorkflows(): Promise<number> {
try {
const now = new Date();
// Find all paused workflows where resume date has passed
// Handle backward compatibility: workflow_type column may not exist in old environments
let pausedWorkflows: WorkflowRequest[];
try {
pausedWorkflows = await WorkflowRequest.findAll({
where: {
isPaused: true,
pauseResumeDate: {
[Op.lte]: now
}
}
});
} catch (error: any) {
// If error is due to missing workflow_type column, use raw query
if (error.message?.includes('workflow_type') || (error.message?.includes('column') && error.message?.includes('does not exist'))) {
logger.warn('[Pause] workflow_type column not found, using raw query for backward compatibility');
const { sequelize } = await import('../config/database');
const { QueryTypes } = await import('sequelize');
const results = await sequelize.query(`
SELECT request_id, is_paused, pause_resume_date
FROM workflow_requests
WHERE is_paused = true
AND pause_resume_date <= :now
`, {
replacements: { now },
type: QueryTypes.SELECT
});
// Convert to WorkflowRequest-like objects
// results is an array of objects from SELECT query
pausedWorkflows = (results as any[]).map((r: any) => ({
requestId: r.request_id,
isPaused: r.is_paused,
pauseResumeDate: r.pause_resume_date
})) as any;
} else {
throw error; // Re-throw if it's a different error
}
}
let resumedCount = 0;
for (const workflow of pausedWorkflows) {
try {
await this.resumeWorkflow((workflow as any).requestId);
resumedCount++;
} catch (error: any) {
logger.error(`[Pause] Failed to auto-resume workflow ${(workflow as any).requestId}:`, error);
// Continue with other workflows
}
}
if (resumedCount > 0) {
logger.info(`[Pause] Auto-resumed ${resumedCount} workflow(s)`);
}
return resumedCount;
} catch (error: any) {
logger.error(`[Pause] Failed to check and resume paused workflows:`, error);
throw error;
}
}
/**
* Get all paused workflows (for admin/reporting)
*/
async getPausedWorkflows(): Promise<WorkflowRequest[]> {
try {
return await WorkflowRequest.findAll({
where: {
isPaused: true
},
order: [['pausedAt', 'DESC']]
});
} catch (error: any) {
logger.error(`[Pause] Failed to get paused workflows:`, error);
throw error;
}
}
}
export const pauseService = new PauseService();