db ssl related changes postman submit bug resolved
This commit is contained in:
parent
e03049a861
commit
b925ee5217
@ -3,6 +3,16 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
// 1. Debugging: Print what the app actually sees
|
||||||
|
console.log('--- Database Config Debug ---');
|
||||||
|
console.log(`DB_HOST: ${process.env.DB_HOST}`);
|
||||||
|
console.log(`DB_SSL (Raw): '${process.env.DB_SSL}`); // Quotes help see trailing spaces
|
||||||
|
|
||||||
|
// 2. Fix: Trim whitespace to ensure "true " becomes "true"
|
||||||
|
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
|
||||||
|
console.log(`SSL Enabled: ${isSSL}`);
|
||||||
|
console.log('---------------------------');
|
||||||
|
|
||||||
const sequelize = new Sequelize({
|
const sequelize = new Sequelize({
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||||
@ -10,7 +20,7 @@ const sequelize = new Sequelize({
|
|||||||
username: process.env.DB_USER || 'postgres',
|
username: process.env.DB_USER || 'postgres',
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
logging: false, // Disable SQL query logging for cleaner console output
|
logging: false,
|
||||||
pool: {
|
pool: {
|
||||||
min: parseInt(process.env.DB_POOL_MIN || '2', 10),
|
min: parseInt(process.env.DB_POOL_MIN || '2', 10),
|
||||||
max: parseInt(process.env.DB_POOL_MAX || '10', 10),
|
max: parseInt(process.env.DB_POOL_MAX || '10', 10),
|
||||||
@ -18,7 +28,8 @@ const sequelize = new Sequelize({
|
|||||||
idle: 10000,
|
idle: 10000,
|
||||||
},
|
},
|
||||||
dialectOptions: {
|
dialectOptions: {
|
||||||
ssl: process.env.DB_SSL === 'true' ? {
|
// 3. Use the robust boolean we calculated above
|
||||||
|
ssl: isSSL ? {
|
||||||
require: true,
|
require: true,
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
} : false,
|
} : false,
|
||||||
|
|||||||
@ -236,6 +236,7 @@ export class WorkflowController {
|
|||||||
priority: validated.priority as Priority,
|
priority: validated.priority as Priority,
|
||||||
approvalLevels: enrichedApprovalLevels,
|
approvalLevels: enrichedApprovalLevels,
|
||||||
participants: autoGeneratedParticipants,
|
participants: autoGeneratedParticipants,
|
||||||
|
isDraft: parsed.isDraft === true, // Submit by default unless isDraft is explicitly true
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const requestMeta = getRequestMetadata(req);
|
const requestMeta = getRequestMetadata(req);
|
||||||
@ -682,7 +683,10 @@ export class WorkflowController {
|
|||||||
}
|
}
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
const validated = validateUpdateWorkflow(parsed);
|
const validated = validateUpdateWorkflow(parsed);
|
||||||
const updateData: UpdateWorkflowRequest = { ...validated } as any;
|
const updateData: UpdateWorkflowRequest = {
|
||||||
|
...validated,
|
||||||
|
isDraft: parsed.isDraft !== undefined ? (parsed.isDraft === true) : undefined
|
||||||
|
} as any;
|
||||||
if (validated.priority) {
|
if (validated.priority) {
|
||||||
updateData.priority = validated.priority === 'EXPRESS' ? Priority.EXPRESS : Priority.STANDARD;
|
updateData.priority = validated.priority === 'EXPRESS' ? Priority.EXPRESS : Priority.STANDARD;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
// DB constants moved inside functions to ensure secrets are loaded first
|
// DB constants moved inside functions to ensure secrets are loaded first
|
||||||
|
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
|
||||||
async function checkAndCreateDatabase(): Promise<boolean> {
|
async function checkAndCreateDatabase(): Promise<boolean> {
|
||||||
const DB_HOST = process.env.DB_HOST || 'localhost';
|
const DB_HOST = process.env.DB_HOST || 'localhost';
|
||||||
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
|
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
|
||||||
@ -36,6 +36,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
user: DB_USER,
|
user: DB_USER,
|
||||||
password: DB_PASSWORD,
|
password: DB_PASSWORD,
|
||||||
database: 'postgres', // Connect to default postgres database
|
database: 'postgres', // Connect to default postgres database
|
||||||
|
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -64,6 +65,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
user: DB_USER,
|
user: DB_USER,
|
||||||
password: DB_PASSWORD,
|
password: DB_PASSWORD,
|
||||||
database: DB_NAME,
|
database: DB_NAME,
|
||||||
|
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await newDbClient.connect();
|
await newDbClient.connect();
|
||||||
|
|||||||
@ -337,7 +337,7 @@ class GoogleSecretManagerService {
|
|||||||
private getDefaultSecretNames(): string[] {
|
private getDefaultSecretNames(): string[] {
|
||||||
return [
|
return [
|
||||||
// Database
|
// Database
|
||||||
//'DB_PASSWORD',
|
'DB_PASSWORD',
|
||||||
|
|
||||||
// JWT & Session
|
// JWT & Session
|
||||||
'JWT_SECRET',
|
'JWT_SECRET',
|
||||||
|
|||||||
@ -2450,6 +2450,9 @@ export class WorkflowService {
|
|||||||
try {
|
try {
|
||||||
const requestNumber = await generateRequestNumber();
|
const requestNumber = await generateRequestNumber();
|
||||||
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
|
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({
|
const workflow = await WorkflowRequest.create({
|
||||||
requestNumber,
|
requestNumber,
|
||||||
@ -2461,9 +2464,10 @@ export class WorkflowService {
|
|||||||
currentLevel: 1,
|
currentLevel: 1,
|
||||||
totalLevels: workflowData.approvalLevels.length,
|
totalLevels: workflowData.approvalLevels.length,
|
||||||
totalTatHours,
|
totalTatHours,
|
||||||
status: WorkflowStatus.DRAFT,
|
status: initialStatus,
|
||||||
isDraft: true,
|
isDraft: isDraftRequested,
|
||||||
isDeleted: false
|
isDeleted: false,
|
||||||
|
submissionDate: isDraftRequested ? undefined : now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create approval levels
|
// Create approval levels
|
||||||
@ -2549,15 +2553,18 @@ export class WorkflowService {
|
|||||||
type: 'created',
|
type: 'created',
|
||||||
user: { userId: initiatorId, name: initiatorName },
|
user: { userId: initiatorId, name: initiatorName },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
action: 'Initial request submitted',
|
action: isDraftRequested ? 'Draft request created' : 'Initial request submitted',
|
||||||
details: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
|
details: isDraftRequested
|
||||||
|
? `Draft request "${workflowData.title}" created by ${initiatorName}`
|
||||||
|
: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
|
||||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||||
userAgent: requestMetadata?.userAgent || undefined
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: Notifications are NOT sent here because workflows are created as DRAFTS
|
// If not a draft, initiate the workflow (approvals, notifications, etc.)
|
||||||
// Notifications will be sent in submitWorkflow() when the draft is actually submitted
|
if (!isDraftRequested) {
|
||||||
// This prevents approvers from being notified about draft requests
|
return await this.internalSubmitWorkflow(workflow, now);
|
||||||
|
}
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -3112,6 +3119,9 @@ export class WorkflowService {
|
|||||||
// Only allow full updates (approval levels, participants) for DRAFT workflows
|
// Only allow full updates (approval levels, participants) for DRAFT workflows
|
||||||
const isDraft = (workflow as any).status === WorkflowStatus.DRAFT || (workflow as any).isDraft;
|
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
|
// Update basic workflow fields
|
||||||
const basicUpdate: any = {};
|
const basicUpdate: any = {};
|
||||||
if (updateData.title) basicUpdate.title = updateData.title;
|
if (updateData.title) basicUpdate.title = updateData.title;
|
||||||
@ -3119,6 +3129,13 @@ export class WorkflowService {
|
|||||||
if (updateData.priority) basicUpdate.priority = updateData.priority;
|
if (updateData.priority) basicUpdate.priority = updateData.priority;
|
||||||
if (updateData.status) basicUpdate.status = updateData.status;
|
if (updateData.status) basicUpdate.status = updateData.status;
|
||||||
if (updateData.conclusionRemark !== undefined) basicUpdate.conclusionRemark = updateData.conclusionRemark;
|
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);
|
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)`);
|
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)
|
// If transitioning, call the internal submission logic (notifications, TAT, etc.)
|
||||||
// The associations issue occurs when trying to include them, so we skip that
|
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);
|
const refreshed = await WorkflowRequest.findByPk(actualRequestId);
|
||||||
return refreshed;
|
return refreshed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -3290,160 +3312,169 @@ export class WorkflowService {
|
|||||||
const workflow = await this.findWorkflowByIdentifier(requestId);
|
const workflow = await this.findWorkflowByIdentifier(requestId);
|
||||||
if (!workflow) return null;
|
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 now = new Date();
|
||||||
const updated = await workflow.update({
|
return await this.internalSubmitWorkflow(workflow, now);
|
||||||
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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to submit workflow ${requestId}:`, error);
|
logger.error(`Failed to submit workflow ${requestId}:`, error);
|
||||||
throw new Error('Failed to submit workflow');
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export interface CreateWorkflowRequest {
|
|||||||
priority: Priority;
|
priority: Priority;
|
||||||
approvalLevels: CreateApprovalLevel[];
|
approvalLevels: CreateApprovalLevel[];
|
||||||
participants?: CreateParticipant[];
|
participants?: CreateParticipant[];
|
||||||
|
isDraft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateWorkflowRequest {
|
export interface UpdateWorkflowRequest {
|
||||||
@ -42,6 +43,7 @@ export interface UpdateWorkflowRequest {
|
|||||||
participants?: CreateParticipant[];
|
participants?: CreateParticipant[];
|
||||||
// Document updates (add new documents via multipart, delete via IDs)
|
// Document updates (add new documents via multipart, delete via IDs)
|
||||||
deleteDocumentIds?: string[];
|
deleteDocumentIds?: string[];
|
||||||
|
isDraft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateApprovalLevel {
|
export interface CreateApprovalLevel {
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export const createWorkflowSchema = z.object({
|
|||||||
priorityUi: z.string().optional(),
|
priorityUi: z.string().optional(),
|
||||||
templateId: z.string().optional(),
|
templateId: z.string().optional(),
|
||||||
ccList: z.array(z.any()).optional(),
|
ccList: z.array(z.any()).optional(),
|
||||||
|
isDraft: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateWorkflowSchema = z.object({
|
export const updateWorkflowSchema = z.object({
|
||||||
@ -73,6 +74,7 @@ export const updateWorkflowSchema = z.object({
|
|||||||
notificationEnabled: z.boolean().optional(),
|
notificationEnabled: z.boolean().optional(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
deleteDocumentIds: z.array(z.string().uuid()).optional(),
|
deleteDocumentIds: z.array(z.string().uuid()).optional(),
|
||||||
|
isDraft: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper to validate UUID or requestNumber format
|
// Helper to validate UUID or requestNumber format
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user