|
|
|
|
@ -2450,6 +2450,9 @@ export class WorkflowService {
|
|
|
|
|
try {
|
|
|
|
|
const requestNumber = await generateRequestNumber();
|
|
|
|
|
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
|
|
|
|
|
const isDraftRequested = workflowData.isDraft === true;
|
|
|
|
|
const initialStatus = isDraftRequested ? WorkflowStatus.DRAFT : WorkflowStatus.PENDING;
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
|
|
const workflow = await WorkflowRequest.create({
|
|
|
|
|
requestNumber,
|
|
|
|
|
@ -2461,9 +2464,10 @@ export class WorkflowService {
|
|
|
|
|
currentLevel: 1,
|
|
|
|
|
totalLevels: workflowData.approvalLevels.length,
|
|
|
|
|
totalTatHours,
|
|
|
|
|
status: WorkflowStatus.DRAFT,
|
|
|
|
|
isDraft: true,
|
|
|
|
|
isDeleted: false
|
|
|
|
|
status: initialStatus,
|
|
|
|
|
isDraft: isDraftRequested,
|
|
|
|
|
isDeleted: false,
|
|
|
|
|
submissionDate: isDraftRequested ? undefined : now
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create approval levels
|
|
|
|
|
@ -2549,15 +2553,18 @@ export class WorkflowService {
|
|
|
|
|
type: 'created',
|
|
|
|
|
user: { userId: initiatorId, name: initiatorName },
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: 'Initial request submitted',
|
|
|
|
|
details: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
|
|
|
|
|
action: isDraftRequested ? 'Draft request created' : 'Initial request submitted',
|
|
|
|
|
details: isDraftRequested
|
|
|
|
|
? `Draft request "${workflowData.title}" created by ${initiatorName}`
|
|
|
|
|
: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
|
|
|
|
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
|
|
|
|
userAgent: requestMetadata?.userAgent || undefined
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// NOTE: Notifications are NOT sent here because workflows are created as DRAFTS
|
|
|
|
|
// Notifications will be sent in submitWorkflow() when the draft is actually submitted
|
|
|
|
|
// This prevents approvers from being notified about draft requests
|
|
|
|
|
// If not a draft, initiate the workflow (approvals, notifications, etc.)
|
|
|
|
|
if (!isDraftRequested) {
|
|
|
|
|
return await this.internalSubmitWorkflow(workflow, now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return workflow;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
@ -3112,6 +3119,9 @@ export class WorkflowService {
|
|
|
|
|
// Only allow full updates (approval levels, participants) for DRAFT workflows
|
|
|
|
|
const isDraft = (workflow as any).status === WorkflowStatus.DRAFT || (workflow as any).isDraft;
|
|
|
|
|
|
|
|
|
|
// Determine if this is a transition from draft to submitted
|
|
|
|
|
const isTransitioningToSubmitted = updateData.isDraft === false && (workflow as any).isDraft;
|
|
|
|
|
|
|
|
|
|
// Update basic workflow fields
|
|
|
|
|
const basicUpdate: any = {};
|
|
|
|
|
if (updateData.title) basicUpdate.title = updateData.title;
|
|
|
|
|
@ -3119,6 +3129,13 @@ export class WorkflowService {
|
|
|
|
|
if (updateData.priority) basicUpdate.priority = updateData.priority;
|
|
|
|
|
if (updateData.status) basicUpdate.status = updateData.status;
|
|
|
|
|
if (updateData.conclusionRemark !== undefined) basicUpdate.conclusionRemark = updateData.conclusionRemark;
|
|
|
|
|
if (updateData.isDraft !== undefined) basicUpdate.isDraft = updateData.isDraft;
|
|
|
|
|
|
|
|
|
|
// If transitioning, ensure status and submissionDate are set
|
|
|
|
|
if (isTransitioningToSubmitted) {
|
|
|
|
|
basicUpdate.status = WorkflowStatus.PENDING;
|
|
|
|
|
basicUpdate.submissionDate = new Date();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await workflow.update(basicUpdate);
|
|
|
|
|
|
|
|
|
|
@ -3267,8 +3284,13 @@ export class WorkflowService {
|
|
|
|
|
logger.info(`Marked ${deleteResult[0]} documents as deleted in database (out of ${updateData.deleteDocumentIds.length} requested)`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reload the workflow instance to get latest data (without associations to avoid the error)
|
|
|
|
|
// The associations issue occurs when trying to include them, so we skip that
|
|
|
|
|
// If transitioning, call the internal submission logic (notifications, TAT, etc.)
|
|
|
|
|
if (isTransitioningToSubmitted) {
|
|
|
|
|
logger.info(`[Workflow] Transitioning draft ${actualRequestId} to submitted state`);
|
|
|
|
|
return await this.internalSubmitWorkflow(workflow, (workflow as any).submissionDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reload the workflow instance to get latest data
|
|
|
|
|
const refreshed = await WorkflowRequest.findByPk(actualRequestId);
|
|
|
|
|
return refreshed;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
@ -3290,160 +3312,169 @@ export class WorkflowService {
|
|
|
|
|
const workflow = await this.findWorkflowByIdentifier(requestId);
|
|
|
|
|
if (!workflow) return null;
|
|
|
|
|
|
|
|
|
|
// Get the actual requestId (UUID) - handle both UUID and requestNumber cases
|
|
|
|
|
const actualRequestId = (workflow as any).getDataValue
|
|
|
|
|
? (workflow as any).getDataValue('requestId')
|
|
|
|
|
: (workflow as any).requestId;
|
|
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const updated = await workflow.update({
|
|
|
|
|
status: WorkflowStatus.PENDING,
|
|
|
|
|
isDraft: false,
|
|
|
|
|
submissionDate: now
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get initiator details for activity logging
|
|
|
|
|
const initiatorId = (updated as any).initiatorId;
|
|
|
|
|
const initiator = initiatorId ? await User.findByPk(initiatorId) : null;
|
|
|
|
|
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
|
|
|
|
|
const workflowTitle = (updated as any).title || 'Request';
|
|
|
|
|
const requestNumber = (updated as any).requestNumber;
|
|
|
|
|
|
|
|
|
|
// Check if this was a previously saved draft (has activity history before submission)
|
|
|
|
|
// or a direct submission (createWorkflow + submitWorkflow in same flow)
|
|
|
|
|
const { Activity } = require('@models/Activity');
|
|
|
|
|
const existingActivities = await Activity.count({
|
|
|
|
|
where: { requestId: actualRequestId }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Only log "Request submitted" if this is a draft being submitted (has prior activities)
|
|
|
|
|
// For direct submissions, createWorkflow already logs "Initial request submitted"
|
|
|
|
|
if (existingActivities > 1) {
|
|
|
|
|
// This is a saved draft being submitted later
|
|
|
|
|
activityService.log({
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
type: 'submitted',
|
|
|
|
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: 'Draft submitted',
|
|
|
|
|
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Direct submission - just update the status, createWorkflow already logged the activity
|
|
|
|
|
activityService.log({
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
type: 'submitted',
|
|
|
|
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: 'Request submitted',
|
|
|
|
|
details: `Request "${workflowTitle}" submitted for approval`
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const current = await ApprovalLevel.findOne({
|
|
|
|
|
where: { requestId: actualRequestId, levelNumber: (updated as any).currentLevel || 1 }
|
|
|
|
|
});
|
|
|
|
|
if (current) {
|
|
|
|
|
// Set the first level's start time and schedule TAT jobs
|
|
|
|
|
await current.update({
|
|
|
|
|
levelStartTime: now,
|
|
|
|
|
tatStartTime: now,
|
|
|
|
|
status: ApprovalStatus.IN_PROGRESS
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Log assignment activity for the first approver (similar to createWorkflow)
|
|
|
|
|
activityService.log({
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
type: 'assignment',
|
|
|
|
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: 'Assigned to approver',
|
|
|
|
|
details: `Request assigned to ${(current as any).approverName || (current as any).approverEmail || 'approver'} for review`
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Schedule TAT notification jobs for the first level
|
|
|
|
|
try {
|
|
|
|
|
const workflowPriority = (updated as any).priority || 'STANDARD';
|
|
|
|
|
await tatSchedulerService.scheduleTatJobs(
|
|
|
|
|
actualRequestId,
|
|
|
|
|
(current as any).levelId,
|
|
|
|
|
(current as any).approverId,
|
|
|
|
|
Number((current as any).tatHours),
|
|
|
|
|
now,
|
|
|
|
|
workflowPriority // Pass workflow priority (EXPRESS = 24/7, STANDARD = working hours)
|
|
|
|
|
);
|
|
|
|
|
logger.info(`[Workflow] TAT jobs scheduled for first level of request ${requestNumber} (Priority: ${workflowPriority})`);
|
|
|
|
|
} catch (tatError) {
|
|
|
|
|
logger.error(`[Workflow] Failed to schedule TAT jobs:`, tatError);
|
|
|
|
|
// Don't fail the submission if TAT scheduling fails
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send notifications when workflow is submitted (not when created as draft)
|
|
|
|
|
// Send notification to INITIATOR confirming submission
|
|
|
|
|
await notificationService.sendToUsers([initiatorId], {
|
|
|
|
|
title: 'Request Submitted Successfully',
|
|
|
|
|
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
|
|
|
|
|
requestNumber: requestNumber,
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
url: `/request/${requestNumber}`,
|
|
|
|
|
type: 'request_submitted',
|
|
|
|
|
priority: 'MEDIUM'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Send notification to FIRST APPROVER for assignment
|
|
|
|
|
await notificationService.sendToUsers([(current as any).approverId], {
|
|
|
|
|
title: 'New Request Assigned',
|
|
|
|
|
body: `${workflowTitle}`,
|
|
|
|
|
requestNumber: requestNumber,
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
url: `/request/${requestNumber}`,
|
|
|
|
|
type: 'assignment',
|
|
|
|
|
priority: 'HIGH',
|
|
|
|
|
actionRequired: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send notifications to SPECTATORS (in-app, email, and web push)
|
|
|
|
|
// Moved outside the if(current) block to ensure spectators are always notified on submission
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`[Workflow] Querying spectators for request ${requestNumber} (requestId: ${actualRequestId})`);
|
|
|
|
|
const spectators = await Participant.findAll({
|
|
|
|
|
where: {
|
|
|
|
|
requestId: actualRequestId, // Use the actual UUID requestId
|
|
|
|
|
participantType: ParticipantType.SPECTATOR,
|
|
|
|
|
isActive: true,
|
|
|
|
|
notificationEnabled: true
|
|
|
|
|
},
|
|
|
|
|
attributes: ['userId', 'userEmail', 'userName']
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info(`[Workflow] Found ${spectators.length} active spectators for request ${requestNumber}`);
|
|
|
|
|
|
|
|
|
|
if (spectators.length > 0) {
|
|
|
|
|
const spectatorUserIds = spectators.map((s: any) => s.userId);
|
|
|
|
|
logger.info(`[Workflow] Sending notifications to ${spectatorUserIds.length} spectators: ${spectatorUserIds.join(', ')}`);
|
|
|
|
|
|
|
|
|
|
await notificationService.sendToUsers(spectatorUserIds, {
|
|
|
|
|
title: 'Added to Request',
|
|
|
|
|
body: `You have been added as a spectator to request ${requestNumber}: ${workflowTitle}`,
|
|
|
|
|
requestNumber: requestNumber,
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
url: `/request/${requestNumber}`,
|
|
|
|
|
type: 'spectator_added',
|
|
|
|
|
priority: 'MEDIUM'
|
|
|
|
|
});
|
|
|
|
|
logger.info(`[Workflow] Successfully sent notifications to ${spectators.length} spectators for request ${requestNumber}`);
|
|
|
|
|
} else {
|
|
|
|
|
logger.info(`[Workflow] No active spectators found for request ${requestNumber} (requestId: ${actualRequestId})`);
|
|
|
|
|
}
|
|
|
|
|
} catch (spectatorError) {
|
|
|
|
|
logger.error(`[Workflow] Failed to send spectator notifications for request ${requestNumber} (requestId: ${actualRequestId}):`, spectatorError);
|
|
|
|
|
// Don't fail the submission if spectator notifications fail
|
|
|
|
|
}
|
|
|
|
|
return updated;
|
|
|
|
|
return await this.internalSubmitWorkflow(workflow, now);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`Failed to submit workflow ${requestId}:`, error);
|
|
|
|
|
throw new Error('Failed to submit workflow');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Internal method to handle workflow submission logic (status update, notifications, TAT scheduling)
|
|
|
|
|
* Centralized here to support both direct creation-submission and draft-to-submission flows.
|
|
|
|
|
*/
|
|
|
|
|
private async internalSubmitWorkflow(workflow: WorkflowRequest, now: Date): Promise<WorkflowRequest> {
|
|
|
|
|
// Get the actual requestId (UUID) - handle both UUID and requestNumber cases
|
|
|
|
|
const actualRequestId = (workflow as any).getDataValue
|
|
|
|
|
? (workflow as any).getDataValue('requestId')
|
|
|
|
|
: (workflow as any).requestId;
|
|
|
|
|
|
|
|
|
|
const updated = await workflow.update({
|
|
|
|
|
status: WorkflowStatus.PENDING,
|
|
|
|
|
isDraft: false,
|
|
|
|
|
submissionDate: now
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Get initiator details for activity logging
|
|
|
|
|
const initiatorId = (updated as any).initiatorId;
|
|
|
|
|
const initiator = initiatorId ? await User.findByPk(initiatorId) : null;
|
|
|
|
|
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
|
|
|
|
|
const workflowTitle = (updated as any).title || 'Request';
|
|
|
|
|
const requestNumber = (updated as any).requestNumber;
|
|
|
|
|
|
|
|
|
|
// Check if this was a previously saved draft (has activity history before submission)
|
|
|
|
|
// or a direct submission (createWorkflow + submitWorkflow in same flow)
|
|
|
|
|
const { Activity } = require('@models/Activity');
|
|
|
|
|
const existingActivities = await Activity.count({
|
|
|
|
|
where: { requestId: actualRequestId }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Only log "Request submitted" if this is a draft being submitted (has prior activities)
|
|
|
|
|
// For direct submissions, createWorkflow already logs "Initial request submitted"
|
|
|
|
|
if (existingActivities > 1) {
|
|
|
|
|
// This is a saved draft being submitted later
|
|
|
|
|
activityService.log({
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
type: 'submitted',
|
|
|
|
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: 'Draft submitted',
|
|
|
|
|
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Direct submission - just update the status, createWorkflow already logged the activity
|
|
|
|
|
activityService.log({
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
type: 'submitted',
|
|
|
|
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: 'Request submitted',
|
|
|
|
|
details: `Request "${workflowTitle}" submitted for approval`
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const current = await ApprovalLevel.findOne({
|
|
|
|
|
where: { requestId: actualRequestId, levelNumber: (updated as any).currentLevel || 1 }
|
|
|
|
|
});
|
|
|
|
|
if (current) {
|
|
|
|
|
// Set the first level's start time and schedule TAT jobs
|
|
|
|
|
await current.update({
|
|
|
|
|
levelStartTime: now,
|
|
|
|
|
tatStartTime: now,
|
|
|
|
|
status: ApprovalStatus.IN_PROGRESS
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Log assignment activity for the first approver (similar to createWorkflow)
|
|
|
|
|
activityService.log({
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
type: 'assignment',
|
|
|
|
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: 'Assigned to approver',
|
|
|
|
|
details: `Request assigned to ${(current as any).approverName || (current as any).approverEmail || 'approver'} for review`
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Schedule TAT notification jobs for the first level
|
|
|
|
|
try {
|
|
|
|
|
const workflowPriority = (updated as any).priority || 'STANDARD';
|
|
|
|
|
await tatSchedulerService.scheduleTatJobs(
|
|
|
|
|
actualRequestId,
|
|
|
|
|
(current as any).levelId,
|
|
|
|
|
(current as any).approverId,
|
|
|
|
|
Number((current as any).tatHours),
|
|
|
|
|
now,
|
|
|
|
|
workflowPriority // Pass workflow priority (EXPRESS = 24/7, STANDARD = working hours)
|
|
|
|
|
);
|
|
|
|
|
logger.info(`[Workflow] TAT jobs scheduled for first level of request ${requestNumber} (Priority: ${workflowPriority})`);
|
|
|
|
|
} catch (tatError) {
|
|
|
|
|
logger.error(`[Workflow] Failed to schedule TAT jobs:`, tatError);
|
|
|
|
|
// Don't fail the submission if TAT scheduling fails
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send notifications when workflow is submitted (not when created as draft)
|
|
|
|
|
// Send notification to INITIATOR confirming submission
|
|
|
|
|
await notificationService.sendToUsers([initiatorId], {
|
|
|
|
|
title: 'Request Submitted Successfully',
|
|
|
|
|
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
|
|
|
|
|
requestNumber: requestNumber,
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
url: `/request/${requestNumber}`,
|
|
|
|
|
type: 'request_submitted',
|
|
|
|
|
priority: 'MEDIUM'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Send notification to FIRST APPROVER for assignment
|
|
|
|
|
await notificationService.sendToUsers([(current as any).approverId], {
|
|
|
|
|
title: 'New Request Assigned',
|
|
|
|
|
body: `${workflowTitle}`,
|
|
|
|
|
requestNumber: requestNumber,
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
url: `/request/${requestNumber}`,
|
|
|
|
|
type: 'assignment',
|
|
|
|
|
priority: 'HIGH',
|
|
|
|
|
actionRequired: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send notifications to SPECTATORS (in-app, email, and web push)
|
|
|
|
|
// Moved outside the if(current) block to ensure spectators are always notified on submission
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`[Workflow] Querying spectators for request ${requestNumber} (requestId: ${actualRequestId})`);
|
|
|
|
|
const spectators = await Participant.findAll({
|
|
|
|
|
where: {
|
|
|
|
|
requestId: actualRequestId, // Use the actual UUID requestId
|
|
|
|
|
participantType: ParticipantType.SPECTATOR,
|
|
|
|
|
isActive: true,
|
|
|
|
|
notificationEnabled: true
|
|
|
|
|
},
|
|
|
|
|
attributes: ['userId', 'userEmail', 'userName']
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info(`[Workflow] Found ${spectators.length} active spectators for request ${requestNumber}`);
|
|
|
|
|
|
|
|
|
|
if (spectators.length > 0) {
|
|
|
|
|
const spectatorUserIds = spectators.map((s: any) => s.userId);
|
|
|
|
|
logger.info(`[Workflow] Sending notifications to ${spectatorUserIds.length} spectators: ${spectatorUserIds.join(', ')}`);
|
|
|
|
|
|
|
|
|
|
await notificationService.sendToUsers(spectatorUserIds, {
|
|
|
|
|
title: 'Added to Request',
|
|
|
|
|
body: `You have been added as a spectator to request ${requestNumber}: ${workflowTitle}`,
|
|
|
|
|
requestNumber: requestNumber,
|
|
|
|
|
requestId: actualRequestId,
|
|
|
|
|
url: `/request/${requestNumber}`,
|
|
|
|
|
type: 'spectator_added',
|
|
|
|
|
priority: 'MEDIUM'
|
|
|
|
|
});
|
|
|
|
|
logger.info(`[Workflow] Successfully sent notifications to ${spectators.length} spectators for request ${requestNumber}`);
|
|
|
|
|
} else {
|
|
|
|
|
logger.info(`[Workflow] No active spectators found for request ${requestNumber} (requestId: ${actualRequestId})`);
|
|
|
|
|
}
|
|
|
|
|
} catch (spectatorError) {
|
|
|
|
|
logger.error(`[Workflow] Failed to send spectator notifications for request ${requestNumber} (requestId: ${actualRequestId}):`, spectatorError);
|
|
|
|
|
// Don't fail the submission if spectator notifications fail
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return updated;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|