added email reinitaialize and api token limit
This commit is contained in:
parent
9285c97d4b
commit
d1ae0ffaec
@ -1,2 +1,2 @@
|
|||||||
import{a as t}from"./index-XMUlTorM.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
import{a as t}from"./index-D5U31xpx.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-muWiQD3D.js.map
|
//# sourceMappingURL=conclusionApi-xBwvOJP0.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-muWiQD3D.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
{"version":3,"file":"conclusionApi-xBwvOJP0.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||||
File diff suppressed because one or more lines are too long
1
build/assets/index-D5U31xpx.js.map
Normal file
1
build/assets/index-D5U31xpx.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -52,7 +52,7 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-XMUlTorM.js"></script>
|
<script type="module" crossorigin src="/assets/index-D5U31xpx.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { seedDefaultConfigurations } from './services/configSeed.service';
|
|||||||
import { startPauseResumeJob } from './jobs/pauseResumeJob';
|
import { startPauseResumeJob } from './jobs/pauseResumeJob';
|
||||||
import './queues/pauseResumeWorker'; // Initialize pause resume worker
|
import './queues/pauseResumeWorker'; // Initialize pause resume worker
|
||||||
import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics';
|
import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics';
|
||||||
|
import { emailService } from './services/email.service';
|
||||||
|
|
||||||
const PORT: number = parseInt(process.env.PORT || '5000', 10);
|
const PORT: number = parseInt(process.env.PORT || '5000', 10);
|
||||||
|
|
||||||
@ -20,6 +21,15 @@ const startServer = async (): Promise<void> => {
|
|||||||
// This will merge secrets from GCS into process.env if enabled
|
// This will merge secrets from GCS into process.env if enabled
|
||||||
await initializeSecrets();
|
await initializeSecrets();
|
||||||
|
|
||||||
|
// Re-initialize email service after secrets are loaded (in case SMTP credentials were loaded)
|
||||||
|
// This ensures the email service uses production SMTP if credentials are available
|
||||||
|
try {
|
||||||
|
await emailService.initialize();
|
||||||
|
console.log('📧 Email service re-initialized after secrets loaded');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Email service re-initialization warning (will use test account if SMTP not configured):', error);
|
||||||
|
}
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
initSocket(server);
|
initSocket(server);
|
||||||
|
|
||||||
|
|||||||
@ -99,10 +99,11 @@ class AIService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the generative model
|
// Get the generative model
|
||||||
|
// Increase maxOutputTokens to handle longer conclusions (up to ~4000 tokens ≈ 3000 words)
|
||||||
const generativeModel = this.vertexAI.getGenerativeModel({
|
const generativeModel = this.vertexAI.getGenerativeModel({
|
||||||
model: this.model,
|
model: this.model,
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
maxOutputTokens: 2048,
|
maxOutputTokens: 4096, // Increased from 2048 to handle longer conclusions
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -154,6 +155,19 @@ class AIService {
|
|||||||
// Extract text from response
|
// Extract text from response
|
||||||
const text = candidate.content?.parts?.[0]?.text || '';
|
const text = candidate.content?.parts?.[0]?.text || '';
|
||||||
|
|
||||||
|
// Handle MAX_TOKENS finish reason - accept whatever response we got
|
||||||
|
// We trust the AI's response - no truncation on our side
|
||||||
|
if (candidate.finishReason === 'MAX_TOKENS' && text) {
|
||||||
|
// Accept the response as-is - AI was instructed to stay within limits
|
||||||
|
// If it hit the limit, we still use what we got (no truncation on our side)
|
||||||
|
logger.info('[AI Service] Vertex AI response hit token limit, but content received is preserved as-is:', {
|
||||||
|
textLength: text.length,
|
||||||
|
finishReason: candidate.finishReason
|
||||||
|
});
|
||||||
|
// Return the response without any truncation - trust what AI generated
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
// Log detailed response structure for debugging
|
// Log detailed response structure for debugging
|
||||||
logger.error('[AI Service] Empty text in Vertex AI response:', {
|
logger.error('[AI Service] Empty text in Vertex AI response:', {
|
||||||
@ -169,7 +183,7 @@ class AIService {
|
|||||||
if (candidate.finishReason === 'SAFETY') {
|
if (candidate.finishReason === 'SAFETY') {
|
||||||
throw new Error('Vertex AI blocked the response due to safety filters. The prompt may contain content that violates safety policies.');
|
throw new Error('Vertex AI blocked the response due to safety filters. The prompt may contain content that violates safety policies.');
|
||||||
} else if (candidate.finishReason === 'MAX_TOKENS') {
|
} else if (candidate.finishReason === 'MAX_TOKENS') {
|
||||||
throw new Error('Vertex AI response was truncated due to token limit.');
|
throw new Error('Vertex AI response was truncated due to token limit. The prompt may be too long or the response limit was exceeded.');
|
||||||
} else if (candidate.finishReason === 'RECITATION') {
|
} else if (candidate.finishReason === 'RECITATION') {
|
||||||
throw new Error('Vertex AI blocked the response due to recitation concerns.');
|
throw new Error('Vertex AI blocked the response due to recitation concerns.');
|
||||||
} else {
|
} else {
|
||||||
@ -254,9 +268,10 @@ class AIService {
|
|||||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||||
|
|
||||||
// Log length (no trimming - preserve complete AI-generated content)
|
// Trust AI's response - do not truncate anything
|
||||||
|
// AI is instructed to stay within limit, but we accept whatever it generates
|
||||||
if (remarkText.length > maxLength) {
|
if (remarkText.length > maxLength) {
|
||||||
logger.warn(`[AI Service] ⚠️ AI exceeded suggested limit (${remarkText.length} > ${maxLength}). Content preserved to avoid incomplete information.`);
|
logger.info(`[AI Service] AI generated ${remarkText.length} characters (suggested limit: ${maxLength}). Full content preserved as-is.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract key points (look for bullet points or numbered items)
|
// Extract key points (look for bullet points or numbered items)
|
||||||
@ -336,8 +351,9 @@ class AIService {
|
|||||||
.map((wn: any) => `- ${wn.userName}: "${wn.message.substring(0, 150)}${wn.message.length > 150 ? '...' : ''}"`)
|
.map((wn: any) => `- ${wn.userName}: "${wn.message.substring(0, 150)}${wn.message.length > 150 ? '...' : ''}"`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
// Summarize documents
|
// Summarize documents (limit to reduce token usage)
|
||||||
const documentSummary = documents
|
const documentSummary = documents
|
||||||
|
.slice(0, 10) // Limit to first 10 documents
|
||||||
.map((d: any) => `- ${d.fileName} (by ${d.uploadedBy})`)
|
.map((d: any) => `- ${d.fileName} (by ${d.uploadedBy})`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
@ -382,15 +398,17 @@ ${isRejected
|
|||||||
- Sounds natural and human-written (not AI-generated)`}
|
- Sounds natural and human-written (not AI-generated)`}
|
||||||
|
|
||||||
**CRITICAL CHARACTER LIMIT - STRICT REQUIREMENT:**
|
**CRITICAL CHARACTER LIMIT - STRICT REQUIREMENT:**
|
||||||
- Your response MUST be EXACTLY within ${maxLength} characters (not words, CHARACTERS including spaces)
|
- Your response MUST stay within ${maxLength} characters (not words, CHARACTERS including spaces including HTML tags)
|
||||||
- Count your characters carefully before responding
|
- This is a HARD LIMIT - you must count your characters and ensure your complete response fits within ${maxLength} characters
|
||||||
|
- Count your characters carefully before responding - include all HTML tags in your count
|
||||||
- If you have too much content, PRIORITIZE the most important information:
|
- If you have too much content, PRIORITIZE the most important information:
|
||||||
1. Final decision (approved/rejected)
|
1. Final decision (approved/rejected)
|
||||||
2. Key approvers and their decisions
|
2. Key approvers and their decisions
|
||||||
3. Critical TAT breaches (if any)
|
3. Critical TAT breaches (if any)
|
||||||
4. Brief summary of the request
|
4. Brief summary of the request
|
||||||
- OMIT less important details to fit within the limit rather than exceeding it
|
- OMIT less important details to fit within the limit rather than exceeding it
|
||||||
- Better to be concise than to exceed the limit
|
- Better to be concise and complete within the limit than to exceed it
|
||||||
|
- IMPORTANT: Generate your complete response within this limit - do not generate partial content that exceeds the limit
|
||||||
|
|
||||||
**WRITING GUIDELINES:**
|
**WRITING GUIDELINES:**
|
||||||
- Be concise and direct - every word must add value
|
- Be concise and direct - every word must add value
|
||||||
|
|||||||
@ -100,6 +100,18 @@ export class EmailService {
|
|||||||
await this.initialize();
|
await this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If using test account, check if SMTP credentials are now available and re-initialize
|
||||||
|
if (this.useTestAccount) {
|
||||||
|
const smtpHost = process.env.SMTP_HOST;
|
||||||
|
const smtpUser = process.env.SMTP_USER;
|
||||||
|
const smtpPassword = process.env.SMTP_PASSWORD;
|
||||||
|
|
||||||
|
if (smtpHost && smtpUser && smtpPassword) {
|
||||||
|
logger.info('📧 SMTP credentials detected - re-initializing email service with production SMTP');
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
||||||
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>';
|
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>';
|
||||||
|
|
||||||
@ -233,6 +245,8 @@ export class EmailService {
|
|||||||
export const emailService = new EmailService();
|
export const emailService = new EmailService();
|
||||||
|
|
||||||
// Initialize on import (will use test account if SMTP not configured)
|
// Initialize on import (will use test account if SMTP not configured)
|
||||||
|
// Note: If secrets are loaded later, the service will re-initialize automatically
|
||||||
|
// when sendEmail is called (if SMTP credentials become available)
|
||||||
emailService.initialize().catch(error => {
|
emailService.initialize().catch(error => {
|
||||||
logger.error('Failed to initialize email service:', error);
|
logger.error('Failed to initialize email service:', error);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1831,14 +1831,16 @@ export class WorkflowService {
|
|||||||
// Include PAUSED status so paused requests where user is the current approver are shown
|
// Include PAUSED status so paused requests where user is the current approver are shown
|
||||||
const pendingLevels = await ApprovalLevel.findAll({
|
const pendingLevels = await ApprovalLevel.findAll({
|
||||||
where: {
|
where: {
|
||||||
status: { [Op.in]: [
|
status: {
|
||||||
|
[Op.in]: [
|
||||||
ApprovalStatus.PENDING as any,
|
ApprovalStatus.PENDING as any,
|
||||||
(ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS',
|
(ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS',
|
||||||
ApprovalStatus.PAUSED as any,
|
ApprovalStatus.PAUSED as any,
|
||||||
'PENDING',
|
'PENDING',
|
||||||
'IN_PROGRESS',
|
'IN_PROGRESS',
|
||||||
'PAUSED'
|
'PAUSED'
|
||||||
] as any },
|
] as any
|
||||||
|
},
|
||||||
},
|
},
|
||||||
order: [['requestId', 'ASC'], ['levelNumber', 'ASC']],
|
order: [['requestId', 'ASC'], ['levelNumber', 'ASC']],
|
||||||
attributes: ['requestId', 'levelNumber', 'approverId'],
|
attributes: ['requestId', 'levelNumber', 'approverId'],
|
||||||
@ -1907,7 +1909,8 @@ export class WorkflowService {
|
|||||||
baseConditions.push({
|
baseConditions.push({
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{
|
{
|
||||||
status: { [Op.in]: [
|
status: {
|
||||||
|
[Op.in]: [
|
||||||
WorkflowStatus.PENDING as any,
|
WorkflowStatus.PENDING as any,
|
||||||
WorkflowStatus.APPROVED as any,
|
WorkflowStatus.APPROVED as any,
|
||||||
WorkflowStatus.PAUSED as any,
|
WorkflowStatus.PAUSED as any,
|
||||||
@ -1915,7 +1918,8 @@ export class WorkflowService {
|
|||||||
'IN_PROGRESS', // Legacy support - will be migrated to PENDING
|
'IN_PROGRESS', // Legacy support - will be migrated to PENDING
|
||||||
'APPROVED',
|
'APPROVED',
|
||||||
'PAUSED'
|
'PAUSED'
|
||||||
] as any }
|
] as any
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// Also include requests with isPaused = true (even if status is PENDING)
|
// Also include requests with isPaused = true (even if status is PENDING)
|
||||||
{
|
{
|
||||||
@ -1942,10 +1946,12 @@ export class WorkflowService {
|
|||||||
baseConditions.push({
|
baseConditions.push({
|
||||||
[Op.and]: [
|
[Op.and]: [
|
||||||
{ status: statusUpper },
|
{ status: statusUpper },
|
||||||
{ [Op.or]: [
|
{
|
||||||
|
[Op.or]: [
|
||||||
{ isPaused: { [Op.is]: null } },
|
{ isPaused: { [Op.is]: null } },
|
||||||
{ isPaused: false }
|
{ isPaused: false }
|
||||||
]}
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -2067,12 +2073,14 @@ export class WorkflowService {
|
|||||||
const levelRows = await ApprovalLevel.findAll({
|
const levelRows = await ApprovalLevel.findAll({
|
||||||
where: {
|
where: {
|
||||||
approverId: userId,
|
approverId: userId,
|
||||||
status: { [Op.in]: [
|
status: {
|
||||||
|
[Op.in]: [
|
||||||
ApprovalStatus.APPROVED as any,
|
ApprovalStatus.APPROVED as any,
|
||||||
(ApprovalStatus as any).REJECTED ?? 'REJECTED',
|
(ApprovalStatus as any).REJECTED ?? 'REJECTED',
|
||||||
'APPROVED',
|
'APPROVED',
|
||||||
'REJECTED'
|
'REJECTED'
|
||||||
] as any },
|
] as any
|
||||||
|
},
|
||||||
},
|
},
|
||||||
attributes: ['requestId'],
|
attributes: ['requestId'],
|
||||||
});
|
});
|
||||||
@ -2378,40 +2386,6 @@ export class WorkflowService {
|
|||||||
userAgent: requestMetadata?.userAgent || undefined
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send notification to INITIATOR confirming submission
|
|
||||||
await notificationService.sendToUsers([initiatorId], {
|
|
||||||
title: 'Request Submitted Successfully',
|
|
||||||
body: `Your request "${workflowData.title}" has been submitted and is now with the first approver.`,
|
|
||||||
requestNumber: requestNumber,
|
|
||||||
requestId: (workflow as any).requestId,
|
|
||||||
url: `/request/${requestNumber}`,
|
|
||||||
type: 'request_submitted',
|
|
||||||
priority: 'MEDIUM'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send notification to FIRST APPROVER for assignment
|
|
||||||
const firstLevel = await ApprovalLevel.findOne({ where: { requestId: (workflow as any).requestId, levelNumber: 1 } });
|
|
||||||
if (firstLevel) {
|
|
||||||
await notificationService.sendToUsers([(firstLevel as any).approverId], {
|
|
||||||
title: 'New Request Assigned',
|
|
||||||
body: `${workflowData.title}`,
|
|
||||||
requestNumber: requestNumber,
|
|
||||||
requestId: (workflow as any).requestId,
|
|
||||||
url: `/request/${requestNumber}`,
|
|
||||||
type: 'assignment',
|
|
||||||
priority: 'HIGH',
|
|
||||||
actionRequired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
activityService.log({
|
|
||||||
requestId: (workflow as any).requestId,
|
|
||||||
type: 'assignment',
|
|
||||||
user: { userId: initiatorId, name: initiatorName },
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'Assigned to approver',
|
|
||||||
details: `Request assigned to ${(firstLevel as any).approverName || (firstLevel as any).approverEmail || 'approver'} for review`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -3192,13 +3166,28 @@ export class WorkflowService {
|
|||||||
// Don't fail the submission if TAT scheduling fails
|
// Don't fail the submission if TAT scheduling fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Notifications are already sent in createWorkflow() when the workflow is created
|
// Send "Request Submitted" notification to INITIATOR
|
||||||
// We should NOT send "Request submitted" to the approver here - that's incorrect
|
await notificationService.sendToUsers([initiatorId], {
|
||||||
// The approver should only receive "New Request Assigned" notification (sent in createWorkflow)
|
title: 'Request Submitted Successfully',
|
||||||
// The initiator receives "Request Submitted Successfully" notification (sent in createWorkflow)
|
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
|
||||||
//
|
requestNumber: (updated as any).requestNumber,
|
||||||
// If this is a draft being submitted, notifications were already sent during creation,
|
requestId: (updated as any).requestId,
|
||||||
// so we don't need to send them again here to avoid duplicates
|
url: `/request/${(updated as any).requestNumber}`,
|
||||||
|
type: 'request_submitted',
|
||||||
|
priority: 'MEDIUM'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send "New Request Assigned" notification to FIRST APPROVER
|
||||||
|
await notificationService.sendToUsers([(current as any).approverId], {
|
||||||
|
title: 'New Request Assigned',
|
||||||
|
body: `${workflowTitle}`,
|
||||||
|
requestNumber: (updated as any).requestNumber,
|
||||||
|
requestId: (updated as any).requestId,
|
||||||
|
url: `/request/${(updated as any).requestNumber}`,
|
||||||
|
type: 'assignment',
|
||||||
|
priority: 'HIGH',
|
||||||
|
actionRequired: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user