fixed smaller issues like undeifned in mails worknote refresh and also some dealer claim rlated changes made stable for the non-templatized save as draft changes

This commit is contained in:
laxmanhalaki 2026-02-05 21:01:31 +05:30
parent 47fabeb15e
commit 5b9012d314
21 changed files with 687 additions and 234 deletions

View File

@ -0,0 +1,41 @@
# MongoDB Atlas v8.0 Readiness Update
**Date**: 2026-02-05
**Project**: Royal Enfield Workflow Management System
**Subject**: Technical Audit and Readiness for MongoDB v8.0 Upgrade
## Executive Summary
Following a comprehensive technical audit of the Workflow Management System backend, we have confirmed that the application layer is fully compatible with MongoDB Atlas v8.0. The current stack (Node.js 22, Mongoose 9) is optimized for the v8 engine, and the codebase has been verified to be free of any deprecated legacy features.
## 💻 Tech Stack Compatibility
| Component | Version | Readiness Status |
| :--- | :--- | :--- |
| **Node.js Runtime** | v22.x | Fully Compatible |
| **Mongoose ODM** | v9.1.5 | Native v8.0 Support |
| **Connection Driver** | MongoDB Node.js Driver v6+ equivalent | Verified |
## 🔍 Codebase Audit Results
### 1. Feature Deprecation Check
We have verified that the following legacy features, removed in v8.0, are **not used** in our codebase:
- **Map-Reduce**: All reporting and KPI logic has been migrated to the modern Aggregation Pipeline.
- **Legacy Group Command**: Using `$group` within aggregation pipelines instead.
- **$where Operator**: All dynamic queries have been refactored to use `$expr` or standard filters to improve performance and security.
- **geoHaystack Indexes**: Not utilized in the project.
### 2. Connection Strategy
Our connection logic is designed for resilient SRV connectivity:
- Implements DNS resolution workarounds for reliable Atlas SRV lookups.
- Configured with robust timeout and selection parameters.
## 🚀 Post-Upgrade Optimization Roadmap
Once the cluster is upgraded to v8.0, the application team recommends the following optimizations:
1. **Atlas Search Integration**: Migrate full-text search requirements from standard regex to Lucene-based Atlas Search.
2. **Encryption**: Evaluate **Queryable Encryption** for enhanced protection of sensitive workflow data.
3. **Performance Advisor**: Review Atlas Performance Advisor recommendations for any new compound index opportunities enabled by the v8 engine's improved query optimizer.
## ✅ Conclusion
The application is **ready for upgrade**. No blockers have been identified in the current production codebase.

View File

@ -0,0 +1,75 @@
# Analysis: Dealer Claim & Unified Request Architecture
This document analyzes the current architecture and proposes an efficient approach to unify Dealer Claims and Custom Requests while supporting specialized data capture and versioning.
## Current State
Both **Custom Requests** and **Dealer Claims** are already "unified" at the base level:
- **Primary Collection**: `workflow_requests` stores the core data (id, requestNumber, initiator, status, currentLevel).
- **Secondary Collection**: `dealer_claims` stores the business-specific metadata (proposal, expenses, invoices, etc.) and is linked via `requestId`.
This architecture naturally supports showing both in the same list.
## Proposed Efficient Approach
To make these two paths truly "inline" and handle specialized steps efficiently, we recommend a **Metadata-Driven Activity System**.
### 1. Unified Listing
The UI should continue to use the existing `listWorkflows` endpoints. The backend already returns `templateType`, which the frontend can use to decide which icon or detail view to render.
### 2. Specialized Step Identification (Dual-Tag System)
To handle dynamic level shifts and accurately recognize the purpose of each step, we use two categories of tags on each `ApprovalLevel`.
#### Category A: Action Tags (`stepAction`)
Defines **what** special behavior is required in this step.
- `DEALER_PROPOSAL`: Show proposal submission form.
- `EXPENSE_CAPTURE`: Show expense document upload form.
- `PROPOSAL_EVALUATION`: Show evaluation tools for the initiator/manager.
- `NONE`: Standard approve/reject UI.
#### Category B: Persona Tags (`stepPersona`)
Defines **who** is acting in this step (role-based logic).
- `INITIATOR`: Used when the initiator acts as an approver (e.g., evaluating a dealer proposal).
- `DEPARTMENT_LEAD`: Standard leadership approval.
- `ADDITIONAL_APPROVER`: Distinguishes steps added manually from the template.
#### How it works together:
| Level | Level Name | `stepAction` | `stepPersona` | UI Behavior |
| :--- | :--- | :--- | :--- | :--- |
| **1** | Dealer Proposal | `DEALER_PROPOSAL` | `DEALER` | Full Proposal Form |
| **2** | Initiator Review | `PROPOSAL_EVALUATION` | `INITIATOR` | Inline evaluation checklist |
| **3** | Lead Approval | `NONE` | `DEPARTMENT_LEAD` | Simple Approve/Reject |
| **3b** | Extra Check | `NONE` | `ADDITIONAL_APPROVER` | Manual Approval UI |
- **Dynamic Insertions**: If `Extra Check` is added, the following levels shift, but their `stepAction` tags remain, so the UI NEVER breaks.
- **Resubmission**: Rejection logic targets the latest completed level with `stepAction: 'DEALER_PROPOSAL'`.
### 3. Versioning & Iterations
The user's requirement to track previous proposals during resubmission is handled via the **Snapshotted Revisions** pattern:
- **The Main Store**: `DealerClaim.proposal` and `DealerClaim.completion` always hold the **active/latest** values.
- **The Revision Store**: `DealerClaim.revisions[]` acts as an append-only audit trail.
**Resubmission Flow:**
1. Request is rejected at Level 2/3/5.
2. Workflow moves back to Level 1 or 4 (Dealer).
3. Dealer edits the data.
4. **On Submit**:
- Backend takes the *current* `proposal` or `completion` data.
- Pushes it into `revisions` with a timestamp and `triggeredBy: 'SYSTEM_VERSION_SNAPSHOT'`.
- Overwrites the main object with the *new* data.
- Advances the workflow.
### 4. Implementation Strategy
| Feature | Custom Request Path | Dealer Claim Path |
| :--- | :--- | :--- |
| **Listing** | Unified `listWorkflows` | Unified `listWorkflows` |
| **Details View** | Standard UI | Enhanced UI (tabs for Expenses/Proposal) |
| **Logic** | Generic `approveRequest` | `approveRequest` + `DealerClaimService` hook |
| **Versioning** | Activity Logs only | Snapshotted Revisions for re-submissions |
---
### Key Advantage
This approach avoids creating "two separate systems". It treats a Dealer Claim as a "Custom Request with a specific metadata payload". The UI remains cohesive, and the backend logic for TAT, notifications, and status transitions stays shared.

View File

@ -1,6 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { WorkflowRequest, ApprovalLevel, WorkNote, Document, Activity, ConclusionRemark, User } from '../models'; // Fixed imports import { WorkflowRequest, ApprovalLevel, WorkNote, Document, Activity, ConclusionRemark, User } from '../models'; // Fixed imports
import { aiService } from '../services/ai.service'; import { aiService } from '../services/ai.service';
import { conclusionMongoService } from '../services/conclusion.service';
import { activityMongoService as activityService } from '../services/activity.service'; import { activityMongoService as activityService } from '../services/activity.service';
import logger from '../utils/logger'; import logger from '../utils/logger';
import { getRequestMetadata } from '../utils/requestUtils'; import { getRequestMetadata } from '../utils/requestUtils';
@ -67,110 +68,18 @@ export class ConclusionController {
}); });
} }
// Gather context for AI generation
// Mongoose: find({ requestId }), sort by levelNumber
const approvalLevels = await ApprovalLevel.find({ requestId })
.sort({ levelNumber: 1 });
const workNotes = await WorkNote.find({ requestId })
.sort({ createdAt: 1 })
.limit(20);
const documents = await Document.find({ requestId })
.sort({ uploadedAt: -1 });
const activities = await Activity.find({ requestId })
.sort({ createdAt: 1 })
.limit(50);
// Fetch initiator details manually since we can't 'include'
const initiator = await User.findOne({ userId: (request as any).initiatorId });
// Build context object
const context = {
requestTitle: (request as any).title,
requestDescription: (request as any).description,
requestNumber: (request as any).requestNumber,
priority: (request as any).priority,
approvalFlow: approvalLevels.map((level: any) => {
const tatPercentage = level.tatPercentageUsed !== undefined && level.tatPercentageUsed !== null
? Number(level.tatPercentageUsed)
: (level.elapsedHours && level.tatHours ? (Number(level.elapsedHours) / Number(level.tatHours)) * 100 : 0);
return {
levelNumber: level.levelNumber,
approverName: level.approver?.name || level.approverName || 'Unknown',
status: level.status,
comments: level.comments,
actionDate: level.actionDate,
tatHours: Number(level.tatHours || 0),
elapsedHours: Number(level.elapsedHours || 0),
tatPercentageUsed: tatPercentage
};
}),
workNotes: workNotes.map((note: any) => ({
userName: note.userName,
message: note.message,
createdAt: note.createdAt
})),
documents: documents.map((doc: any) => ({
fileName: doc.originalFileName || doc.fileName,
uploadedBy: doc.uploadedBy,
uploadedAt: doc.uploadedAt
})),
activities: activities.map((activity: any) => ({
type: activity.activityType,
action: activity.activityDescription,
details: activity.activityDescription,
timestamp: activity.createdAt
}))
};
logger.info(`[Conclusion] Generating AI remark for request ${requestId}...`); logger.info(`[Conclusion] Generating AI remark for request ${requestId}...`);
// Generate AI conclusion // Use the service to generate and save (consistent with automatic trigger)
const aiResult = await aiService.generateConclusionRemark(context); const conclusionInstance = await conclusionMongoService.generateAndSaveAIConclusion(requestId);
// Check if conclusion already exists if (!conclusionInstance) {
let conclusionInstance = await ConclusionRemark.findOne({ requestId }); return res.status(500).json({ error: 'Failed to generate conclusion' });
const conclusionData = {
aiGeneratedRemark: aiResult.remark,
aiModelUsed: aiResult.provider,
aiConfidenceScore: aiResult.confidence,
approvalSummary: {
totalLevels: approvalLevels.length,
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
},
documentSummary: {
totalDocuments: documents.length,
documentNames: documents.map((d: any) => d.originalFileName || d.fileName)
},
keyDiscussionPoints: aiResult.keyPoints,
generatedAt: new Date()
};
if (conclusionInstance) {
// Update existing conclusion (allow regeneration)
// Mongoose document update
Object.assign(conclusionInstance, conclusionData);
await conclusionInstance.save();
logger.info(`[Conclusion] ✅ AI conclusion regenerated for request ${requestId}`);
} else {
// Create new conclusion
conclusionInstance = await ConclusionRemark.create({
requestId,
...conclusionData,
finalRemark: undefined,
editedBy: undefined,
isEdited: false,
editCount: 0,
finalizedAt: undefined
});
logger.info(`[Conclusion] ✅ AI conclusion generated for request ${requestId}`);
} }
// Fetch initiator details manually for logging
const initiator = await User.findOne({ userId: (request as any).initiatorId });
// Log activity // Log activity
const requestMeta = getRequestMetadata(req); const requestMeta = getRequestMetadata(req);
await activityService.log({ await activityService.log({
@ -188,11 +97,11 @@ export class ConclusionController {
message: 'Conclusion generated successfully', message: 'Conclusion generated successfully',
data: { data: {
conclusionId: (conclusionInstance as any).conclusionId || (conclusionInstance as any)._id, conclusionId: (conclusionInstance as any).conclusionId || (conclusionInstance as any)._id,
aiGeneratedRemark: aiResult.remark, aiGeneratedRemark: conclusionInstance.aiGeneratedRemark,
keyDiscussionPoints: aiResult.keyPoints, keyDiscussionPoints: conclusionInstance.keyDiscussionPoints,
confidence: aiResult.confidence, confidence: conclusionInstance.aiConfidenceScore,
provider: aiResult.provider, provider: conclusionInstance.aiModelUsed,
generatedAt: new Date() generatedAt: conclusionInstance.generatedAt
} }
}); });
} catch (error: any) { } catch (error: any) {

View File

@ -1,5 +1,6 @@
import { Response } from 'express'; import { Response } from 'express';
import { pauseMongoService } from '@services/pause.service'; import { pauseMongoService } from '@services/pause.service';
import { workflowServiceMongo } from '@services/workflow.service';
import { ResponseHandler } from '@utils/responseHandler'; import { ResponseHandler } from '@utils/responseHandler';
import type { AuthenticatedRequest } from '../types/express'; import type { AuthenticatedRequest } from '../types/express';
import { z } from 'zod'; import { z } from 'zod';
@ -31,6 +32,13 @@ export class PauseController {
return; return;
} }
// Resolve requestId (UUID)
const requestId = await workflowServiceMongo.resolveRequestId(id);
if (!requestId) {
ResponseHandler.notFound(res, 'Request not found');
return;
}
// Validate request body // Validate request body
const validated = pauseWorkflowSchema.parse(req.body); const validated = pauseWorkflowSchema.parse(req.body);
const resumeDate = validated.resumeDate instanceof Date const resumeDate = validated.resumeDate instanceof Date
@ -38,7 +46,7 @@ export class PauseController {
: new Date(validated.resumeDate); : new Date(validated.resumeDate);
const result = await pauseMongoService.pauseWorkflow( const result = await pauseMongoService.pauseWorkflow(
id, requestId,
validated.levelId || null, validated.levelId || null,
userId, userId,
validated.reason, validated.reason,
@ -73,10 +81,17 @@ export class PauseController {
return; return;
} }
// Resolve requestId (UUID)
const requestId = await workflowServiceMongo.resolveRequestId(id);
if (!requestId) {
ResponseHandler.notFound(res, 'Request not found');
return;
}
// Validate request body (notes is optional) // Validate request body (notes is optional)
const validated = resumeWorkflowSchema.parse(req.body || {}); const validated = resumeWorkflowSchema.parse(req.body || {});
const result = await pauseMongoService.resumeWorkflow(id, userId, validated.notes); const result = await pauseMongoService.resumeWorkflow(requestId, userId, validated.notes);
ResponseHandler.success(res, { ResponseHandler.success(res, {
workflow: result.workflow, workflow: result.workflow,
@ -106,7 +121,14 @@ export class PauseController {
return; return;
} }
await pauseMongoService.retriggerPause(id, userId); // Resolve requestId (UUID)
const requestId = await workflowServiceMongo.resolveRequestId(id);
if (!requestId) {
ResponseHandler.notFound(res, 'Request not found');
return;
}
await pauseMongoService.retriggerPause(requestId, userId);
ResponseHandler.success(res, null, 'Pause retrigger request sent successfully', 200); ResponseHandler.success(res, null, 'Pause retrigger request sent successfully', 200);
} catch (error: any) { } catch (error: any) {
@ -123,7 +145,14 @@ export class PauseController {
try { try {
const { id } = req.params; const { id } = req.params;
const pauseDetails = await pauseMongoService.getPauseDetails(id); // Resolve requestId (UUID)
const requestId = await workflowServiceMongo.resolveRequestId(id);
if (!requestId) {
ResponseHandler.notFound(res, 'Request not found');
return;
}
const pauseDetails = await pauseMongoService.getPauseDetails(requestId);
if (!pauseDetails) { if (!pauseDetails) {
ResponseHandler.success(res, { isPaused: false }, 'Workflow is not paused', 200); ResponseHandler.success(res, { isPaused: false }, 'Workflow is not paused', 200);

View File

@ -113,6 +113,18 @@ export class WorkflowController {
userAgent: requestMeta.userAgent userAgent: requestMeta.userAgent
}); });
// Handle auto-submit unless isDraft is true
const isDraftRequested = req.body.isDraft === true; // Default to false if not explicitly true
if (!isDraftRequested) {
logger.info(`[WorkflowController] Auto-submitting workflow ${workflow.requestNumber}`);
await workflowServiceMongo.submitWorkflow(workflow.requestId);
// Refetch to get updated status/state
const updatedWorkflow = await workflowServiceMongo.getWorkflowById(workflow.requestId);
ResponseHandler.success(res, updatedWorkflow, 'Workflow created and submitted successfully', 201);
return;
}
ResponseHandler.success(res, workflow, 'Workflow created successfully', 201); ResponseHandler.success(res, workflow, 'Workflow created successfully', 201);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@ -406,6 +418,23 @@ export class WorkflowController {
} }
} }
// Handle auto-submit unless isDraft is true
const isDraftRequested = parsed.isDraft === true; // Default to false if not explicitly true
if (!isDraftRequested) {
logger.info(`[WorkflowController] Auto-submitting multipart workflow ${workflow.requestNumber}`);
await workflowServiceMongo.submitWorkflow(workflow.requestId);
// Get updated workflow to return complete state
const updatedWorkflow = await workflowServiceMongo.getWorkflowById(workflow.requestId);
ResponseHandler.success(res, {
requestId: workflow.requestNumber,
status: updatedWorkflow.status,
workflowState: updatedWorkflow.workflowState,
documents: docs
}, 'Workflow created and submitted with documents', 201);
return;
}
ResponseHandler.success(res, { requestId: workflow.requestNumber, documents: docs }, 'Workflow created with documents', 201); ResponseHandler.success(res, { requestId: workflow.requestNumber, documents: docs }, 'Workflow created with documents', 201);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@ -663,6 +692,19 @@ export class WorkflowController {
return; return;
} }
// Handle auto-submit if isDraft is explicitly false or missing
// If it's already submitted, submitWorkflow will throw or handle gracefully
const isDraftRequested = req.body.isDraft === true; // Default to false
if (!isDraftRequested && (workflow as any).isDraft) {
logger.info(`[WorkflowController] Auto-submitting workflow ${workflow.requestNumber} after update`);
await workflowServiceMongo.submitWorkflow(workflow.requestId);
// Refetch updated workflow
const updatedWorkflow = await workflowServiceMongo.getWorkflowById(workflow.requestId);
ResponseHandler.success(res, updatedWorkflow, 'Workflow updated and submitted successfully');
return;
}
ResponseHandler.success(res, workflow, 'Workflow updated successfully'); ResponseHandler.success(res, workflow, 'Workflow updated successfully');
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@ -858,6 +900,18 @@ export class WorkflowController {
} }
} }
// Handle auto-submit if isDraft is false
const isDraftRequested = parsed.isDraft === true; // Default to false
if (!isDraftRequested && (workflow as any).isDraft) {
logger.info(`[WorkflowController] Auto-submitting multipart workflow ${workflow.requestNumber} after update`);
await workflowServiceMongo.submitWorkflow(workflow.requestId);
// Return updated workflow
const updatedWorkflow = await workflowServiceMongo.getWorkflowById(workflow.requestId);
ResponseHandler.success(res, updatedWorkflow, 'Workflow updated and submitted successfully');
return;
}
ResponseHandler.success(res, { workflow, newDocuments: docs }, 'Workflow updated with documents', 200); ResponseHandler.success(res, { workflow, newDocuments: docs }, 'Workflow updated with documents', 200);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@ -920,13 +974,12 @@ export class WorkflowController {
try { try {
const { id } = req.params; const { id } = req.params;
// Resolve requestId from identifier (could be requestNumber or ID) // Resolve requestId from identifier (could be requestNumber or UUID)
const wf = await workflowServiceMongo.getRequest(id); const requestId = await workflowServiceMongo.resolveRequestId(id);
if (!wf) { if (!requestId) {
ResponseHandler.notFound(res, 'Workflow not found'); ResponseHandler.notFound(res, 'Workflow not found');
return; return;
} }
const requestId = wf.requestId; // Use UUID
const history = await dealerClaimService.getHistory(requestId); const history = await dealerClaimService.getHistory(requestId);
ResponseHandler.success(res, history, 'Revision history fetched successfully'); ResponseHandler.success(res, history, 'Revision history fetched successfully');

View File

@ -1,7 +1,6 @@
import type { Response } from 'express'; import type { Response } from 'express';
import { workNoteMongoService } from '../services/worknote.service'; import { workNoteMongoService } from '../services/worknote.service';
import { workflowServiceMongo } from '../services/workflow.service'; import { workflowServiceMongo } from '../services/workflow.service';
import { getRequestMetadata } from '@utils/requestUtils';
import { ResponseHandler } from '@utils/responseHandler'; import { ResponseHandler } from '@utils/responseHandler';
import { AuthenticatedRequest } from '../types/express'; import { AuthenticatedRequest } from '../types/express';
import { ParticipantModel } from '../models/mongoose/Participant.schema'; import { ParticipantModel } from '../models/mongoose/Participant.schema';
@ -12,15 +11,15 @@ export class WorkNoteController {
*/ */
async list(req: AuthenticatedRequest, res: Response): Promise<void> { async list(req: AuthenticatedRequest, res: Response): Promise<void> {
try { try {
const requestNumber = req.params.id; const identifier = req.params.id; // Could be requestNumber or UUID
const request = await workflowServiceMongo.getRequest(requestNumber); const requestId = await workflowServiceMongo.resolveRequestId(identifier);
if (!request) { if (!requestId) {
ResponseHandler.notFound(res, 'Request not found'); ResponseHandler.notFound(res, 'Request not found');
return; return;
} }
const rows = await workNoteMongoService.list(requestNumber); const rows = await workNoteMongoService.list(requestId);
ResponseHandler.success(res, rows, 'Work notes retrieved'); ResponseHandler.success(res, rows, 'Work notes retrieved');
} catch (error) { } catch (error) {
ResponseHandler.error(res, 'Failed to list work notes', 500); ResponseHandler.error(res, 'Failed to list work notes', 500);
@ -32,17 +31,17 @@ export class WorkNoteController {
*/ */
async create(req: AuthenticatedRequest, res: Response): Promise<void> { async create(req: AuthenticatedRequest, res: Response): Promise<void> {
try { try {
const requestNumber = req.params.id; const identifier = req.params.id; // Could be requestNumber or UUID
const request = await workflowServiceMongo.getRequest(requestNumber); const requestId = await workflowServiceMongo.resolveRequestId(identifier);
if (!request) { if (!requestId) {
ResponseHandler.notFound(res, 'Request not found'); ResponseHandler.notFound(res, 'Request not found');
return; return;
} }
// Get user's participant info from Mongo // Get user's participant info from Mongo using UUID
const participant = await ParticipantModel.findOne({ const participant = await ParticipantModel.findOne({
requestId: requestNumber, requestId: requestId,
userId: req.user.userId userId: req.user.userId
}); });
@ -78,9 +77,8 @@ export class WorkNoteController {
mentionedUsers: payload.mentions || [] mentionedUsers: payload.mentions || []
}; };
const requestMeta = getRequestMetadata(req);
const note = await workNoteMongoService.create( const note = await workNoteMongoService.create(
requestNumber, requestId,
user, user,
workNotePayload, workNotePayload,
files files

View File

@ -55,7 +55,13 @@ export interface IDealer extends Document {
} }
const DealerSchema = new Schema<IDealer>({ const DealerSchema = new Schema<IDealer>({
dealerId: { type: String, required: true, unique: true, index: true }, dealerId: {
type: String,
required: true,
unique: true,
index: true,
default: () => require('crypto').randomUUID()
},
// Codes // Codes
salesCode: { type: String, index: true }, salesCode: { type: String, index: true },

View File

@ -6,29 +6,60 @@ const seedTestDealerMongo = async () => {
try { try {
await connectMongoDB(); await connectMongoDB();
const dealerData = { const dealerData: any = {
dlrcode: 'TEST001', // Changed from dealerCode salesCode: 'TEST001',
dealerId: 'test-dealer-id', // Added explicit ID or auto-gen handled by schema? Schema requires dealerId. serviceCode: 'TEST001',
dealerName: 'TEST REFLOW DEALERSHIP', gearCode: 'TEST001',
gmaCode: 'TEST001',
region: 'TEST', region: 'TEST',
dealership: 'TEST REFLOW DEALERSHIP',
state: 'Test State', state: 'Test State',
district: 'Test District',
city: 'Test City', city: 'Test City',
zone: 'Test Zone',
location: 'Test Location', location: 'Test Location',
sapCode: 'SAP001', cityCategoryPst: 'A',
email: 'testreflow@example.com', layoutFormat: 'A',
phone: '9999999999', tierCityCategory: 'Tier 1 City',
address: 'Test Address, Test City', onBoardingCharges: null,
isActive: true, date: new Date().toISOString().split('T')[0],
// Additional fields can be added if schema supports them singleFormatMonthYear: (() => {
const now = new Date();
const month = now.toLocaleDateString('en-US', { month: 'short' });
const year = now.getFullYear();
return `${month}-${year}`;
})(),
domainId: 'testreflow@example.com',
replacement: null,
terminationResignationStatus: null,
dateOfTerminationResignation: null,
lastDateOfOperations: null,
oldCodes: null,
branchDetails: null,
dealerPrincipalName: 'TEST REFLOW',
dealerPrincipalEmailId: 'testreflow@example.com',
dpContactNumber: null,
dpContacts: null,
showroomAddress: null,
showroomPincode: null,
workshopAddress: null,
workshopPincode: null,
locationDistrict: null,
stateWorkshop: null,
noOfStudios: 0,
websiteUpdate: 'Yes',
gst: null,
pan: null,
firmType: 'Test Firm',
propManagingPartnersDirectors: 'TEST REFLOW',
totalPropPartnersDirectors: 'TEST REFLOW',
docsFolderLink: null,
workshopGmaCodes: null,
existingNew: 'New',
dlrcode: 'TEST001',
isActive: true
}; };
const existingDealer = await DealerModel.findOne({ const existingDealer = await DealerModel.findOne({ dlrcode: dealerData.dlrcode });
$or: [
{ dlrcode: dealerData.dlrcode },
{ email: dealerData.email }
]
});
if (existingDealer) { if (existingDealer) {
logger.info('[Seed Test Dealer Mongo] Dealer already exists, updating...'); logger.info('[Seed Test Dealer Mongo] Dealer already exists, updating...');
@ -36,13 +67,10 @@ const seedTestDealerMongo = async () => {
await existingDealer.save(); await existingDealer.save();
logger.info(`[Seed Test Dealer Mongo] ✅ Updated dealer: ${existingDealer.dlrcode}`); logger.info(`[Seed Test Dealer Mongo] ✅ Updated dealer: ${existingDealer.dlrcode}`);
} else { } else {
// Ensure dealerId is present if required const newDealer = await DealerModel.create({
if (!dealerData.dealerId) { ...dealerData,
// Generate a UUID if not provided? dealerId: require('crypto').randomUUID()
// The interface has dealerId required. });
// Let's add it to dealerData above.
}
const newDealer = await DealerModel.create(dealerData);
logger.info(`[Seed Test Dealer Mongo] ✅ Created dealer: ${newDealer.dlrcode}`); logger.info(`[Seed Test Dealer Mongo] ✅ Created dealer: ${newDealer.dlrcode}`);
} }

View File

@ -0,0 +1,40 @@
import mongoose from 'mongoose';
import { DealerModel } from '../models/mongoose/Dealer.schema';
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.join(__dirname, '../../.env') });
async function verifyDealerIdGen() {
try {
console.log('Connecting to MongoDB...');
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/re_workflow_db';
await mongoose.connect(mongoUri);
console.log('Connected.');
console.log('Attempting to create a dealer document without dealerId...');
const testDealerData = {
salesCode: 'S' + Math.floor(Math.random() * 10000),
dealership: 'Verification Test Dealership',
isActive: true
};
const dealer = new DealerModel(testDealerData);
console.log('Constructed dealer object dealerId:', dealer.dealerId);
if (dealer.dealerId) {
console.log('✅ Success: dealerId was automatically generated!');
console.log('Generated ID:', dealer.dealerId);
} else {
console.error('❌ Failure: dealerId was NOT generated.');
}
await mongoose.disconnect();
process.exit(0);
} catch (error) {
console.error('❌ Error during verification:', error);
process.exit(1);
}
}
verifyDealerIdGen();

View File

@ -2,6 +2,7 @@ import { ApprovalLevelModel } from '../models/mongoose/ApprovalLevel.schema';
import { WorkflowRequestModel } from '../models/mongoose/WorkflowRequest.schema'; import { WorkflowRequestModel } from '../models/mongoose/WorkflowRequest.schema';
import { ApprovalAction } from '../types/approval.types'; import { ApprovalAction } from '../types/approval.types';
import { ApprovalStatus, WorkflowStatus } from '../types/common.types'; import { ApprovalStatus, WorkflowStatus } from '../types/common.types';
import { conclusionMongoService } from './conclusion.service';
import logger from '../utils/logger'; import logger from '../utils/logger';
export class ApprovalService { export class ApprovalService {
@ -53,6 +54,11 @@ export class ApprovalService {
wf.closureDate = now; wf.closureDate = now;
await wf.save(); await wf.save();
// Trigger AI conclusion generation
conclusionMongoService.generateAndSaveAIConclusion(wf.requestId).catch(err => {
logger.error(`[ApprovalService] Deferred AI conclusion error for ${wf.requestId} (Rejection):`, err);
});
// Notify initiator // Notify initiator
await notificationMongoService.sendToUsers([wf.initiator.userId], { await notificationMongoService.sendToUsers([wf.initiator.userId], {
title: `Request Rejected: ${wf.requestNumber}`, title: `Request Rejected: ${wf.requestNumber}`,
@ -88,10 +94,12 @@ export class ApprovalService {
level.comments = action.comments; level.comments = action.comments;
await level.save(); await level.save();
// Check if this is the final approval // Check for final approver
const allLevels = await ApprovalLevelModel.find({ requestId: wf.requestId }); const allLevels = await ApprovalLevelModel.find({ requestId: level.requestId });
const approvedCount = allLevels.filter(l => l.status === ApprovalStatus.APPROVED).length; const completedCount = allLevels.filter(l =>
const isFinal = approvedCount === allLevels.length; l.status === ApprovalStatus.APPROVED || l.status === ApprovalStatus.SKIPPED
).length;
const isFinal = completedCount === allLevels.length;
if (isFinal) { if (isFinal) {
// Final approval - close workflow // Final approval - close workflow
@ -126,6 +134,17 @@ export class ApprovalService {
category: 'WORKFLOW', category: 'WORKFLOW',
severity: 'INFO' severity: 'INFO'
}); });
// Trigger automatic AI conclusion generation
try {
// Running as "fire and forget" or wait?
// Usually better to let it run in background to not block the response
conclusionMongoService.generateAndSaveAIConclusion(wf.requestId).catch(err => {
logger.error(`[ApprovalService] Deferred AI conclusion error for ${wf.requestId}:`, err);
});
} catch (aiError) {
logger.error(`[ApprovalService] AI conclusion trigger failed for ${wf.requestId}:`, aiError);
}
} else { } else {
// Move to next level // Move to next level
const currentLevelNum = level.levelNumber; const currentLevelNum = level.levelNumber;

View File

@ -0,0 +1,137 @@
import { WorkflowRequest, ApprovalLevel, WorkNote, Document, Activity, ConclusionRemark, User } from '../models';
import { aiService } from './ai.service';
import logger from '../utils/logger';
import { v4 as uuidv4 } from 'uuid';
export class ConclusionMongoService {
/**
* Generate and save AI conclusion for a request
*/
async generateAndSaveAIConclusion(requestId: string): Promise<any> {
try {
logger.info(`[Conclusion Service] Starting automatic AI conclusion generation for request ${requestId}`);
// 1. Fetch Request
const request = await WorkflowRequest.findOne({ requestId });
if (!request) {
throw new Error('Request not found');
}
// Check if AI features are enabled in admin config
const { getConfigValue } = await import('./configReader.service');
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
if (!aiEnabled || !remarkGenerationEnabled) {
logger.warn(`[Conclusion Service] AI generation disabled in admin config for request ${requestId}`);
return null;
}
// Check if AI service is available
if (!aiService.isAvailable()) {
logger.warn(`[Conclusion Service] AI service unavailable for request ${requestId}`);
return null;
}
// 2. Gather context for AI generation
const approvalLevels = await ApprovalLevel.find({ requestId })
.sort({ levelNumber: 1 });
const workNotes = await WorkNote.find({ requestId })
.sort({ createdAt: 1 })
.limit(20);
const documents = await Document.find({ requestId })
.sort({ uploadedAt: -1 });
const activities = await Activity.find({ requestId })
.sort({ createdAt: 1 })
.limit(50);
// 3. Build context object
const context = {
requestTitle: (request as any).title,
requestDescription: (request as any).description,
requestNumber: (request as any).requestNumber,
priority: (request as any).priority,
approvalFlow: approvalLevels.map((level: any) => {
const tatPercentage = level.tatPercentageUsed !== undefined && level.tatPercentageUsed !== null
? Number(level.tatPercentageUsed)
: (level.elapsedHours && level.tatHours ? (Number(level.elapsedHours) / Number(level.tatHours)) * 100 : 0);
return {
levelNumber: level.levelNumber,
approverName: level.approver?.name || level.approverName || 'Unknown',
status: level.status,
comments: level.comments,
actionDate: level.actionDate,
tatHours: Number(level.tatHours || 0),
elapsedHours: Number(level.elapsedHours || 0),
tatPercentageUsed: tatPercentage
};
}),
workNotes: workNotes.map((note: any) => ({
userName: note.userName,
message: note.message,
createdAt: note.createdAt
})),
documents: documents.map((doc: any) => ({
fileName: doc.originalFileName || doc.fileName,
uploadedBy: doc.uploadedBy,
uploadedAt: doc.uploadedAt
})),
activities: activities.map((activity: any) => ({
type: activity.activityType,
action: activity.activityDescription,
details: activity.activityDescription,
timestamp: activity.createdAt
}))
};
// 4. Generate AI conclusion via AI Service
const aiResult = await aiService.generateConclusionRemark(context);
// 5. Save/Update ConclusionRemark
let conclusionInstance = await ConclusionRemark.findOne({ requestId });
const conclusionData = {
aiGeneratedRemark: aiResult.remark,
aiModelUsed: aiResult.provider,
aiConfidenceScore: aiResult.confidence,
approvalSummary: {
totalLevels: approvalLevels.length,
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
},
documentSummary: {
totalDocuments: documents.length,
documentNames: documents.map((d: any) => d.originalFileName || d.fileName)
},
keyDiscussionPoints: aiResult.keyPoints,
generatedAt: new Date()
};
if (conclusionInstance) {
Object.assign(conclusionInstance, conclusionData);
await conclusionInstance.save();
} else {
conclusionInstance = await ConclusionRemark.create({
requestId,
conclusionId: uuidv4(),
...conclusionData,
isEdited: false,
editCount: 0
});
}
logger.info(`[Conclusion Service] ✅ Automatic AI conclusion generated for request ${requestId}`);
return conclusionInstance;
} catch (error) {
logger.error(`[Conclusion Service] ❌ Failed to generate automatic AI conclusion for ${requestId}:`, error);
// We don't re-throw as this is a background/automatic process that shouldn't break the main flow
return null;
}
}
}
export const conclusionMongoService = new ConclusionMongoService();

View File

@ -8,8 +8,9 @@ import { DocumentModel } from '../models/mongoose/Document.schema';
import { DealerClaimApprovalMongoService } from './dealerClaimApproval.service'; import { DealerClaimApprovalMongoService } from './dealerClaimApproval.service';
import { WorkflowServiceMongo } from './workflow.service'; import { WorkflowServiceMongo } from './workflow.service';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { notificationMongoService } from './notification.service';
import { activityMongoService } from './activity.service'; import { activityMongoService } from './activity.service';
import { conclusionMongoService } from './conclusion.service';
import { notificationMongoService } from './notification.service';
import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types'; import { Priority, WorkflowStatus, ApprovalStatus, ParticipantType } from '../types/common.types';
import logger from '../utils/logger'; import logger from '../utils/logger';
import { generateRequestNumber } from '../utils/helpers'; import { generateRequestNumber } from '../utils/helpers';
@ -87,6 +88,7 @@ export class DealerClaimMongoService {
description: claimData.requestDescription, description: claimData.requestDescription,
priority: Priority.STANDARD, priority: Priority.STANDARD,
status: WorkflowStatus.PENDING, status: WorkflowStatus.PENDING,
workflowState: 'OPEN',
totalLevels: 5, totalLevels: 5,
currentLevel: 1, currentLevel: 1,
totalTatHours: 0, // Will be calculated totalTatHours: 0, // Will be calculated
@ -325,7 +327,7 @@ export class DealerClaimMongoService {
percentageUsed: 0, percentageUsed: 0,
isBreached: false isBreached: false
}, },
status: isStep1 ? 'PENDING' : 'PENDING', // Archived code sets Step 1 to PENDING too, but effectively it's the active one status: isStep1 ? 'IN_PROGRESS' : 'PENDING', // Active step 1 starts as IN_PROGRESS
// Wait. Usually Step 1 should be IN_PROGRESS if it's active. // Wait. Usually Step 1 should be IN_PROGRESS if it's active.
// In the archived code: `status: isStep1 ? ApprovalStatus.PENDING : ApprovalStatus.PENDING` - both pending? // In the archived code: `status: isStep1 ? ApprovalStatus.PENDING : ApprovalStatus.PENDING` - both pending?
// Ah, `dealerLevel` is later used. // Ah, `dealerLevel` is later used.
@ -611,10 +613,14 @@ export class DealerClaimMongoService {
// Update workflow status // Update workflow status
workflow.status = 'REJECTED'; workflow.status = 'REJECTED';
workflow.workflowState = 'CLOSED'; workflow.workflowState = 'CLOSED';
workflow.isDeleted = true; // Soft delete or just mark cancelled? Usually cancelled.
// Let's stick to status update. // Let's stick to status update.
await workflow.save(); await workflow.save();
// Trigger AI conclusion generation
conclusionMongoService.generateAndSaveAIConclusion(workflow.requestId).catch(err => {
logger.error(`[DealerClaimService] Deferred AI conclusion error for ${workflow.requestId} (Cancel):`, err);
});
// Log activity // Log activity
const user = await UserModel.findOne({ userId }); const user = await UserModel.findOne({ userId });
const userName = user?.displayName || user?.email || 'User'; const userName = user?.displayName || user?.email || 'User';

View File

@ -10,6 +10,7 @@ import { calculateElapsedWorkingHours } from '../utils/tatTimeUtils';
import logger from '../utils/logger'; import logger from '../utils/logger';
import { notificationMongoService } from './notification.service'; import { notificationMongoService } from './notification.service';
import { activityMongoService } from './activity.service'; import { activityMongoService } from './activity.service';
import { conclusionMongoService } from './conclusion.service';
import { tatSchedulerMongoService } from './tatScheduler.service'; import { tatSchedulerMongoService } from './tatScheduler.service';
import { DealerClaimMongoService } from './dealerClaim.service'; import { DealerClaimMongoService } from './dealerClaim.service';
import { emitToRequestRoom } from '../realtime/socket'; import { emitToRequestRoom } from '../realtime/socket';
@ -78,8 +79,10 @@ export class DealerClaimApprovalMongoService {
// Check for final approver // Check for final approver
const allLevels = await ApprovalLevelModel.find({ requestId: level.requestId }); const allLevels = await ApprovalLevelModel.find({ requestId: level.requestId });
const approvedCount = allLevels.filter(l => l.status === ApprovalStatus.APPROVED).length; const completedCount = allLevels.filter(l =>
const isFinal = approvedCount === allLevels.length; l.status === ApprovalStatus.APPROVED || l.status === ApprovalStatus.SKIPPED
).length;
const isFinal = completedCount === allLevels.length;
if (isFinal) { if (isFinal) {
wf.status = WorkflowStatus.APPROVED; wf.status = WorkflowStatus.APPROVED;
@ -99,6 +102,11 @@ export class DealerClaimApprovalMongoService {
type: 'approval', type: 'approval',
priority: 'MEDIUM' priority: 'MEDIUM'
}); });
// Trigger automatic AI conclusion generation
conclusionMongoService.generateAndSaveAIConclusion(level.requestId).catch(err => {
logger.error(`[DealerClaimApprovalService] Deferred AI conclusion error for ${level.requestId}:`, err);
});
} else { } else {
// Move to next level // Move to next level
const currentLevelNum = level.levelNumber; const currentLevelNum = level.levelNumber;
@ -212,6 +220,11 @@ export class DealerClaimApprovalMongoService {
// level.rejectionReason = action.rejectionReason; // Assuming field exists // level.rejectionReason = action.rejectionReason; // Assuming field exists
await level.save(); await level.save();
// Trigger AI conclusion generation
conclusionMongoService.generateAndSaveAIConclusion(wf.requestId).catch(err => {
logger.error(`[DealerClaimApprovalService] Deferred AI conclusion error for ${wf.requestId} (Rejection):`, err);
});
// Notify // Notify
await notificationMongoService.sendToUsers([wf.initiator.userId], { await notificationMongoService.sendToUsers([wf.initiator.userId], {
title: `Request Rejected: ${wf.requestNumber}`, title: `Request Rejected: ${wf.requestNumber}`,

View File

@ -161,12 +161,12 @@ export class EmailNotificationService {
if (isMultiLevel && approvalChain) { if (isMultiLevel && approvalChain) {
// Multi-level approval email // Multi-level approval email
const chainData: ApprovalChainItem[] = approvalChain.map((level: any) => ({ const chainData: ApprovalChainItem[] = approvalChain.map((level: any) => ({
name: level.approverName || level.approverEmail, name: level.approver?.name || level.approverName || level.approver?.email || level.approverEmail || 'Unknown Approver',
status: level.status === 'APPROVED' ? 'approved' status: level.status === 'APPROVED' ? 'approved'
: level.levelNumber === approverData.levelNumber ? 'current' : level.levelNumber === approverData.levelNumber ? 'current'
: level.levelNumber < approverData.levelNumber ? 'pending' : level.levelNumber < approverData.levelNumber ? 'pending'
: 'awaiting', : 'awaiting',
date: level.approvedAt ? this.formatDate(level.approvedAt) : undefined, date: (level.actionDate || level.approvedAt) ? this.formatDate(level.actionDate || level.approvedAt) : undefined,
levelNumber: level.levelNumber levelNumber: level.levelNumber
})); }));
@ -175,7 +175,7 @@ export class EmailNotificationService {
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
requestTitle: requestData.title, requestTitle: requestData.title,
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: requestData.initiator?.name || initiatorData.displayName || initiatorData.email || 'Initiator',
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType), requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
requestDescription: requestData.description || '', requestDescription: requestData.description || '',
priority: requestData.priority || 'MEDIUM', priority: requestData.priority || 'MEDIUM',
@ -207,7 +207,7 @@ export class EmailNotificationService {
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
requestTitle: requestData.title, requestTitle: requestData.title,
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: requestData.initiator?.name || initiatorData.displayName || initiatorData.email || 'Initiator',
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType), requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
requestDescription: requestData.description || '', requestDescription: requestData.description || '',
priority: requestData.priority || 'MEDIUM', priority: requestData.priority || 'MEDIUM',
@ -259,14 +259,14 @@ export class EmailNotificationService {
const data: ApprovalConfirmationData = { const data: ApprovalConfirmationData = {
recipientName: initiatorData.displayName || initiatorData.email, recipientName: initiatorData.displayName || initiatorData.email,
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: requestData.initiator?.name || initiatorData.displayName || initiatorData.email || 'Initiator',
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email || approverData.name,
approvalDate: this.formatDate(approverData.approvedAt || new Date()), approvalDate: this.formatDate(approverData.approvedAt || approverData.actionDate || new Date()),
approvalTime: this.formatTime(approverData.approvedAt || new Date()), approvalTime: this.formatTime(approverData.approvedAt || approverData.actionDate || new Date()),
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType), requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
approverComments: approverData.comments || undefined, approverComments: approverData.comments || undefined,
isFinalApproval, isFinalApproval,
nextApproverName: nextApproverData?.displayName || nextApproverData?.email, nextApproverName: nextApproverData?.displayName || nextApproverData?.email || nextApproverData?.name,
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name companyName: CompanyInfo.name
}; };
@ -312,10 +312,10 @@ export class EmailNotificationService {
const data: RejectionNotificationData = { const data: RejectionNotificationData = {
recipientName: initiatorData.displayName || initiatorData.email, recipientName: initiatorData.displayName || initiatorData.email,
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: requestData.initiator?.name || initiatorData.displayName || initiatorData.email || 'Initiator',
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email || approverData.name,
rejectionDate: this.formatDate(approverData.rejectedAt || new Date()), rejectionDate: this.formatDate(approverData.rejectedAt || approverData.actionDate || new Date()),
rejectionTime: this.formatTime(approverData.rejectedAt || new Date()), rejectionTime: this.formatTime(approverData.rejectedAt || approverData.actionDate || new Date()),
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType), requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
rejectionReason, rejectionReason,
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
@ -388,7 +388,7 @@ export class EmailNotificationService {
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
requestTitle: requestData.title, requestTitle: requestData.title,
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
initiatorName: initiatorName, initiatorName: requestData.initiator?.name || initiatorName,
assignedDate: this.formatDate(tatInfo.assignedDate), assignedDate: this.formatDate(tatInfo.assignedDate),
tatDeadline: this.formatDate(tatInfo.tatDeadline) + ' ' + this.formatTime(tatInfo.tatDeadline), tatDeadline: this.formatDate(tatInfo.tatDeadline) + ' ' + this.formatTime(tatInfo.tatDeadline),
timeRemaining: tatInfo.timeRemaining, timeRemaining: tatInfo.timeRemaining,
@ -459,7 +459,7 @@ export class EmailNotificationService {
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
requestTitle: requestData.title, requestTitle: requestData.title,
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
initiatorName: initiatorName, initiatorName: requestData.initiator?.name || initiatorName,
priority: requestData.priority || 'MEDIUM', priority: requestData.priority || 'MEDIUM',
assignedDate: this.formatDate(tatInfo.assignedDate), assignedDate: this.formatDate(tatInfo.assignedDate),
tatDeadline: this.formatDate(tatInfo.tatDeadline) + ' ' + this.formatTime(tatInfo.tatDeadline), tatDeadline: this.formatDate(tatInfo.tatDeadline) + ' ' + this.formatTime(tatInfo.tatDeadline),
@ -987,8 +987,9 @@ export class EmailNotificationService {
// Add current approvers if they are not the uploader // Add current approvers if they are not the uploader
if (requestData.approvalLevels && Array.isArray(requestData.approvalLevels)) { if (requestData.approvalLevels && Array.isArray(requestData.approvalLevels)) {
const currentLevel = requestData.approvalLevels.find((l: any) => l.status === 'PENDING' || l.status === 'IN_PROGRESS'); const currentLevel = requestData.approvalLevels.find((l: any) => l.status === 'PENDING' || l.status === 'IN_PROGRESS');
if (currentLevel && currentLevel.approverEmail && currentLevel.approverEmail !== uploaderData.email) { const approverEmail = currentLevel?.approver?.email || currentLevel?.approverEmail;
recipients.add(currentLevel.approverEmail); if (approverEmail && approverEmail !== uploaderData.email) {
recipients.add(approverEmail);
} }
} }
@ -1050,8 +1051,8 @@ export class EmailNotificationService {
requestTitle: requestData.title, requestTitle: requestData.title,
participantName: recipientData.displayName || recipientData.email, participantName: recipientData.displayName || recipientData.email,
participantRole: 'Approver', participantRole: 'Approver',
addedByName: addedByData.displayName || addedByData.email, addedByName: addedByData.displayName || addedByData.email || addedByData.name,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: requestData.initiator?.name || initiatorData.displayName || initiatorData.email || 'Initiator',
requestType: getTemplateTypeLabel(requestData.templateType), requestType: getTemplateTypeLabel(requestData.templateType),
currentStatus: requestData.status, currentStatus: requestData.status,
addedDate: this.formatDate(new Date()), addedDate: this.formatDate(new Date()),
@ -1098,8 +1099,8 @@ export class EmailNotificationService {
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
requestTitle: requestData.title, requestTitle: requestData.title,
spectatorName: recipientData.displayName || recipientData.email, spectatorName: recipientData.displayName || recipientData.email,
addedByName: addedByData.displayName || addedByData.email, addedByName: addedByData.displayName || addedByData.email || addedByData.name,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: requestData.initiator?.name || initiatorData.displayName || initiatorData.email || 'Initiator',
requestType: getTemplateTypeLabel(requestData.templateType), requestType: getTemplateTypeLabel(requestData.templateType),
currentStatus: requestData.status, currentStatus: requestData.status,
addedDate: this.formatDate(new Date()), addedDate: this.formatDate(new Date()),

View File

@ -531,8 +531,8 @@ class NotificationMongoService {
await emailNotificationService.sendRejectionNotification( await emailNotificationService.sendRejectionNotification(
requestData, requestData,
initiatorData,
approverData, approverData,
initiatorData,
payload.metadata?.rejectionReason || 'No reason provided' payload.metadata?.rejectionReason || 'No reason provided'
); );
} }

View File

@ -31,13 +31,8 @@ export class PauseMongoService {
throw new Error('Resume date must be in the future'); throw new Error('Resume date must be in the future');
} }
// Get workflow by requestNumber or requestId (both are UUID strings) // Get workflow by requestId (UUID)
let workflow: any = await WorkflowRequestModel.findOne({ let workflow: any = await WorkflowRequestModel.findOne({ requestId });
$or: [
{ requestNumber: requestId },
{ requestId: requestId }
]
});
if (!workflow) throw new Error('Workflow not found'); if (!workflow) throw new Error('Workflow not found');
@ -122,7 +117,7 @@ export class PauseMongoService {
await workflow.save(); await workflow.save();
// Cancel jobs // Cancel jobs
await tatScheduler.cancelTatJobs(workflow.requestNumber, level._id.toString()); await tatScheduler.cancelTatJobs(workflow.requestId, level._id.toString());
// Notifications // Notifications
const user = await UserModel.findOne({ userId }); const user = await UserModel.findOne({ userId });
@ -132,7 +127,7 @@ export class PauseMongoService {
await notificationMongoService.sendToUsers([initiatorId], { await notificationMongoService.sendToUsers([initiatorId], {
title: 'Workflow Paused', title: 'Workflow Paused',
body: `Your request "${workflow.title}" has been paused by ${userName}. Reason: ${reason}.`, body: `Your request "${workflow.title}" has been paused by ${userName}. Reason: ${reason}.`,
requestId: workflow.requestNumber, requestId: workflow.requestId,
requestNumber: workflow.requestNumber, requestNumber: workflow.requestNumber,
type: 'workflow_paused', type: 'workflow_paused',
priority: 'HIGH', priority: 'HIGH',
@ -144,7 +139,7 @@ export class PauseMongoService {
await notificationMongoService.sendToUsers([userId], { await notificationMongoService.sendToUsers([userId], {
title: 'Workflow Paused Successfully', title: 'Workflow Paused Successfully',
body: `You have paused request "${workflow.title}".`, body: `You have paused request "${workflow.title}".`,
requestId: workflow.requestNumber, requestId: workflow.requestId,
requestNumber: workflow.requestNumber, requestNumber: workflow.requestNumber,
type: 'status_change', type: 'status_change',
priority: 'MEDIUM' priority: 'MEDIUM'
@ -154,16 +149,16 @@ export class PauseMongoService {
await notificationMongoService.sendToUsers([approverId], { await notificationMongoService.sendToUsers([approverId], {
title: 'Workflow Paused by Initiator', title: 'Workflow Paused by Initiator',
body: `Request "${workflow.title}" has been paused by the initiator.`, body: `Request "${workflow.title}" has been paused by the initiator.`,
requestId: workflow.requestNumber, requestId: workflow.requestId,
requestNumber: workflow.requestNumber, requestNumber: workflow.requestNumber,
type: 'workflow_paused', type: 'workflow_paused',
priority: 'HIGH' priority: 'HIGH'
}); });
} }
// Log Activity // Log Activity - Standardized to UUID
await activityMongoService.log({ await activityMongoService.log({
requestId: workflow.requestNumber, requestId: workflow.requestId,
type: 'paused', type: 'paused',
user: { userId, name: userName }, user: { userId, name: userName },
timestamp: now.toISOString(), timestamp: now.toISOString(),
@ -179,12 +174,12 @@ export class PauseMongoService {
if (pauseResumeQueue && resumeDate) { if (pauseResumeQueue && resumeDate) {
const delay = resumeDate.getTime() - now.getTime(); const delay = resumeDate.getTime() - now.getTime();
if (delay > 0) { if (delay > 0) {
const jobId = `resume-${workflow.requestNumber}-${level._id.toString()}`; const jobId = `resume-${workflow.requestId}-${level._id.toString()}`;
await pauseResumeQueue.add( await pauseResumeQueue.add(
'auto-resume-workflow', 'auto-resume-workflow',
{ {
type: 'auto-resume-workflow', type: 'auto-resume-workflow',
requestId: workflow.requestNumber, // Use semantic ID requestId: workflow.requestId, // Standardized to UUID
levelId: level._id.toString(), levelId: level._id.toString(),
scheduledResumeDate: resumeDate.toISOString() scheduledResumeDate: resumeDate.toISOString()
}, },
@ -199,8 +194,9 @@ export class PauseMongoService {
try { try {
const { emitToRequestRoom } = require('../realtime/socket'); const { emitToRequestRoom } = require('../realtime/socket');
if (emitToRequestRoom) { if (emitToRequestRoom) {
emitToRequestRoom(workflow.requestNumber, 'request:updated', { emitToRequestRoom(workflow.requestId, 'request:updated', {
requestId: workflow.requestNumber, requestId: workflow.requestId,
requestNumber: workflow.requestNumber,
action: 'PAUSE', action: 'PAUSE',
timestamp: now.toISOString() timestamp: now.toISOString()
}); });
@ -265,7 +261,7 @@ export class PauseMongoService {
workflow.pausedBy = undefined; workflow.pausedBy = undefined;
workflow.pauseReason = undefined; workflow.pauseReason = undefined;
workflow.pauseResumeDate = undefined; workflow.pauseResumeDate = undefined;
workflow.status = 'IN_PROGRESS'; // Assuming previous status was IN_PROGRESS or PENDING workflow.status = 'PENDING'; // Resumed requests return to PENDING
workflow.workflowState = 'OPEN'; workflow.workflowState = 'OPEN';
await workflow.save(); await workflow.save();
@ -273,7 +269,7 @@ export class PauseMongoService {
try { try {
const { pauseResumeQueue } = require('../queues/pauseResumeQueue'); const { pauseResumeQueue } = require('../queues/pauseResumeQueue');
if (pauseResumeQueue) { if (pauseResumeQueue) {
const jobId = `resume-${workflow.requestNumber}-${level._id.toString()}`; const jobId = `resume-${workflow.requestId}-${level._id.toString()}`;
const specificJob = await pauseResumeQueue.getJob(jobId); const specificJob = await pauseResumeQueue.getJob(jobId);
if (specificJob) await specificJob.remove(); if (specificJob) await specificJob.remove();
} }
@ -289,7 +285,7 @@ export class PauseMongoService {
// We need alert status from level // We need alert status from level
const alerts = level.alerts || {}; const alerts = level.alerts || {};
await tatScheduler.scheduleTatJobsOnResume( await tatScheduler.scheduleTatJobsOnResume(
workflow.requestNumber, workflow.requestId,
level._id.toString(), level._id.toString(),
level.approver?.userId || '', level.approver?.userId || '',
remainingHours, remainingHours,
@ -316,7 +312,7 @@ export class PauseMongoService {
await notificationMongoService.sendToUsers([initiatorId], { await notificationMongoService.sendToUsers([initiatorId], {
title: 'Workflow Resumed', title: 'Workflow Resumed',
body: `Your request "${workflow.title}" has been resumed ${userId ? `by ${userName}` : 'automatically'}.`, body: `Your request "${workflow.title}" has been resumed ${userId ? `by ${userName}` : 'automatically'}.`,
requestId: workflow.requestNumber, requestId: workflow.requestId,
requestNumber: workflow.requestNumber, requestNumber: workflow.requestNumber,
type: 'workflow_resumed', type: 'workflow_resumed',
priority: 'HIGH', priority: 'HIGH',
@ -328,16 +324,16 @@ export class PauseMongoService {
await notificationMongoService.sendToUsers([userId], { await notificationMongoService.sendToUsers([userId], {
title: 'Workflow Resumed Successfully', title: 'Workflow Resumed Successfully',
body: `You have resumed request "${workflow.title}".`, body: `You have resumed request "${workflow.title}".`,
requestId: workflow.requestNumber, requestId: workflow.requestId,
requestNumber: workflow.requestNumber, requestNumber: workflow.requestNumber,
type: 'status_change', type: 'status_change',
priority: 'MEDIUM' priority: 'MEDIUM'
}); });
} }
// Log Activity // Log Activity - Standardized to UUID
await activityMongoService.log({ await activityMongoService.log({
requestId: workflow.requestNumber, requestId: workflow.requestId,
type: 'resumed', type: 'resumed',
user: userId ? { userId, name: userName } : undefined, user: userId ? { userId, name: userName } : undefined,
timestamp: now.toISOString(), timestamp: now.toISOString(),
@ -350,8 +346,9 @@ export class PauseMongoService {
try { try {
const { emitToRequestRoom } = require('../realtime/socket'); const { emitToRequestRoom } = require('../realtime/socket');
if (emitToRequestRoom) { if (emitToRequestRoom) {
emitToRequestRoom(workflow.requestNumber, 'request:updated', { emitToRequestRoom(workflow.requestId, 'request:updated', {
requestId: workflow.requestNumber, requestId: workflow.requestId,
requestNumber: workflow.requestNumber,
action: 'RESUME', action: 'RESUME',
timestamp: now.toISOString() timestamp: now.toISOString()
}); });
@ -389,14 +386,15 @@ export class PauseMongoService {
await notificationMongoService.sendToUsers([pausedBy], { await notificationMongoService.sendToUsers([pausedBy], {
title: 'Pause Retrigger Request', title: 'Pause Retrigger Request',
body: `${initiator?.displayName || 'The initiator'} is requesting you to resume work on request "${workflow.title}".`, body: `${initiator?.displayName || 'The initiator'} is requesting you to resume work on request "${workflow.title}".`,
requestId, requestId: workflow.requestId,
requestNumber: workflow.requestNumber, requestNumber: workflow.requestNumber,
url: `/request/${workflow.requestNumber}`, url: `/request/${workflow.requestNumber}`,
type: 'pause_retrigger_request' type: 'pause_retrigger_request'
}); });
// Log Activity - Standardized to UUID
await activityMongoService.log({ await activityMongoService.log({
requestId: workflow.requestNumber, requestId: workflow.requestId,
type: 'pause_retriggered', type: 'pause_retriggered',
user: { userId, name: initiator?.displayName || 'Initiator' }, user: { userId, name: initiator?.displayName || 'Initiator' },
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),

View File

@ -339,6 +339,10 @@ export class SummaryService {
async getSummaryDetailsBySharedId(sharedSummaryId: string, userId: string): Promise<any> { async getSummaryDetailsBySharedId(sharedSummaryId: string, userId: string): Promise<any> {
// Search for request summary containing this shared item // Search for request summary containing this shared item
// sharedSummaryId might be the subdoc _id // sharedSummaryId might be the subdoc _id
if (!mongoose.Types.ObjectId.isValid(sharedSummaryId)) {
throw new Error('Shared link not valid (format)');
}
const summary = await RequestSummary.findOne({ "sharedWith._id": sharedSummaryId }); const summary = await RequestSummary.findOne({ "sharedWith._id": sharedSummaryId });
if (!summary) throw new Error('Shared link not valid'); if (!summary) throw new Error('Shared link not valid');

View File

@ -8,6 +8,7 @@ import logger from '../utils/logger';
import { notificationMongoService } from './notification.service'; import { notificationMongoService } from './notification.service';
import { activityMongoService } from './activity.service'; import { activityMongoService } from './activity.service';
import { tatSchedulerMongoService } from './tatScheduler.service'; import { tatSchedulerMongoService } from './tatScheduler.service';
import { conclusionMongoService } from './conclusion.service';
import { addWorkingHours, addWorkingHoursExpress, calculateSLAStatus, formatTime } from '../utils/tatTimeUtils'; import { addWorkingHours, addWorkingHoursExpress, calculateSLAStatus, formatTime } from '../utils/tatTimeUtils';
const tatScheduler = tatSchedulerMongoService; const tatScheduler = tatSchedulerMongoService;
@ -48,6 +49,14 @@ export class WorkflowServiceMongo {
return WorkflowServiceMongo._supportsTransactions; return WorkflowServiceMongo._supportsTransactions;
} }
/**
* Public helper to resolve requestId (UUID) from either UUID or requestNumber
*/
async resolveRequestId(identifier: string): Promise<string | null> {
const request = await this.findRequest(identifier);
return request ? request.requestId : null;
}
/** /**
* Internal helper to find a workflow request by either UUID or request number * Internal helper to find a workflow request by either UUID or request number
*/ */
@ -307,7 +316,7 @@ export class WorkflowServiceMongo {
// Update Parent Request // Update Parent Request
request.currentLevel = nextLevelNum; request.currentLevel = nextLevelNum;
request.status = 'IN_PROGRESS'; request.status = 'PENDING';
await request.save(); await request.save();
// SCHEDULE TAT for Next Level // SCHEDULE TAT for Next Level
@ -378,6 +387,11 @@ export class WorkflowServiceMongo {
actionRequired: false actionRequired: false
}); });
// Trigger automatic AI conclusion generation
conclusionMongoService.generateAndSaveAIConclusion(request.requestId).catch(err => {
logger.error(`[WorkflowService] Deferred AI conclusion error for ${request.requestId}:`, err);
});
return `Approved Level ${currentLevelNum}. Workflow COMPLETED.`; return `Approved Level ${currentLevelNum}. Workflow COMPLETED.`;
} }
@ -421,6 +435,11 @@ export class WorkflowServiceMongo {
request.conclusionRemark = comments; request.conclusionRemark = comments;
await request.save(); await request.save();
// Trigger AI conclusion generation
conclusionMongoService.generateAndSaveAIConclusion(request.requestId).catch(err => {
logger.error(`[WorkflowService] Deferred AI conclusion error for ${request.requestId} (Rejection):`, err);
});
// Fetch rejecter // Fetch rejecter
const rejecter = await UserModel.findOne({ userId }); const rejecter = await UserModel.findOne({ userId });
@ -700,7 +719,7 @@ export class WorkflowServiceMongo {
await nextLevel.save(sessionOpt); await nextLevel.save(sessionOpt);
request.currentLevel = nextLevelNum; request.currentLevel = nextLevelNum;
request.status = 'IN_PROGRESS'; request.status = 'PENDING';
await request.save(sessionOpt); await request.save(sessionOpt);
// Schedule TAT for next level (if outside transaction) // Schedule TAT for next level (if outside transaction)
@ -712,6 +731,14 @@ export class WorkflowServiceMongo {
request.closureDate = new Date(); request.closureDate = new Date();
request.conclusionRemark = 'Workflow Completed (skipped final level)'; request.conclusionRemark = 'Workflow Completed (skipped final level)';
await request.save(sessionOpt); await request.save(sessionOpt);
// Trigger AI conclusion generation (after session commit if outside transaction, or here if inside)
// Note: If using transaction, might be better to trigger after commit
if (!useTransaction) {
conclusionMongoService.generateAndSaveAIConclusion(request.requestId).catch(err => {
logger.error(`[WorkflowService] Deferred AI conclusion error for ${request.requestId} (Complete):`, err);
});
}
} }
if (useTransaction) await session.commitTransaction(); if (useTransaction) await session.commitTransaction();
@ -760,6 +787,11 @@ export class WorkflowServiceMongo {
requestNumber: request.requestNumber, requestNumber: request.requestNumber,
actionRequired: false actionRequired: false
}); });
// Trigger automatic AI conclusion generation
conclusionMongoService.generateAndSaveAIConclusion(request.requestId).catch(err => {
logger.error(`[WorkflowService] Deferred AI conclusion error for ${request.requestId} (After skipped final level):`, err);
});
} }
return level; return level;
@ -1277,7 +1309,7 @@ export class WorkflowServiceMongo {
} }
// 4. Sort & Pagination // 4. Sort & Pagination
const sortField = sortBy || 'createdAt'; const sortField = sortBy === 'created' ? 'createdAt' : (sortBy || 'createdAt');
const sortDir = sortOrder?.toLowerCase() === 'asc' ? 1 : -1; const sortDir = sortOrder?.toLowerCase() === 'asc' ? 1 : -1;
pipeline.push( pipeline.push(
@ -1906,7 +1938,7 @@ export class WorkflowServiceMongo {
const activatedLevel1 = await ApprovalLevelModel.findOneAndUpdate( const activatedLevel1 = await ApprovalLevelModel.findOneAndUpdate(
{ requestId: workflow.requestId, levelNumber: 1 }, { requestId: workflow.requestId, levelNumber: 1 },
{ {
status: 'PENDING', status: 'IN_PROGRESS',
'tat.startTime': now, 'tat.startTime': now,
'tat.endTime': endTime 'tat.endTime': endTime
}, },

View File

@ -103,21 +103,24 @@ export class WorkNoteMongoService {
} }
} }
// 3. Log Activity // 3. (REMOVED) Log Activity - Redundant as worknotes are managed separately
await activityMongoService.log({
requestId,
type: 'comment',
user: { userId: user.userId, name: user.name || 'User' },
timestamp: new Date().toISOString(),
action: 'Activity',
details: `${user.name || 'User'} added a work note: ${payload.message.substring(0, 100)}`,
category: 'COMMENT',
severity: 'INFO'
});
// 4. Send Notifications // 4. Send Notifications
await this.handleNotifications(requestId, user, payload, note); await this.handleNotifications(requestId, user, payload, note);
// 5. Emit Real-time Socket Update for "Smooth Chat"
try {
const { emitToRequestRoom } = require('../realtime/socket');
const socketPayload = {
...note.toJSON(),
attachments: await this.getAttachments(note.noteId)
};
emitToRequestRoom(requestId, 'worknote:new', socketPayload);
logger.info(`[WorkNote] Emitted real-time update to room request:${requestId}`);
} catch (socketError) {
logger.warn('[WorkNote] Failed to emit socket update:', socketError);
}
return note; return note;
} catch (error) { } catch (error) {
logger.error('[WorkNote Mongo Service] Error creating note:', error); logger.error('[WorkNote Mongo Service] Error creating note:', error);
@ -170,10 +173,12 @@ export class WorkNoteMongoService {
*/ */
private async handleNotifications(requestId: string, user: any, payload: any, note: any) { private async handleNotifications(requestId: string, user: any, payload: any, note: any) {
try { try {
const workflow = await WorkflowRequestModel.findOne({ requestNumber: requestId }); // Identifier is guaranteed to be UUID here
const workflow = await WorkflowRequestModel.findOne({ requestId });
if (!workflow) return; if (!workflow) return;
const recipients: string[] = []; const recipients: string[] = [];
const mentionedUsersList = payload.mentionedUsers || [];
// Basic logic: notify initiator if someone else posts, notify current approver if initiator posts // Basic logic: notify initiator if someone else posts, notify current approver if initiator posts
if (user.userId !== workflow.initiator.userId) { if (user.userId !== workflow.initiator.userId) {
@ -185,7 +190,7 @@ export class WorkNoteMongoService {
levelNumber: workflow.currentLevel levelNumber: workflow.currentLevel
}); });
if (currentLevel && currentLevel.approver.userId !== user.userId) { if (currentLevel && currentLevel.approver?.userId !== user.userId) {
recipients.push(currentLevel.approver.userId); recipients.push(currentLevel.approver.userId);
} }
@ -202,22 +207,41 @@ export class WorkNoteMongoService {
} }
} }
// Add mentioned users // Handle Mentions Specifically
if (payload.mentionedUsers?.length) { if (mentionedUsersList.length > 0) {
payload.mentionedUsers.forEach((uid: string) => { const messageSnippet = payload.message.length > 50
if (!recipients.includes(uid) && uid !== user.userId) { ? payload.message.substring(0, 47) + '...'
recipients.push(uid); : payload.message;
const mentionPayload = {
title: '💬 Mentioned in Work Note',
body: `${user.name || 'A user'} mentioned you in ${workflow.requestNumber}: "${messageSnippet}"`,
requestId,
requestNumber: workflow.requestNumber,
url: `/request/${workflow.requestNumber}`,
type: 'mention',
metadata: { noteId: note.noteId }
};
// Notify only mentioned users with the mention context
for (const uid of mentionedUsersList) {
if (uid !== user.userId) {
await notificationMongoService.sendToUsers([uid], mentionPayload);
// Also remove from general recipients to avoid double notification
const idx = recipients.indexOf(uid);
if (idx > -1) recipients.splice(idx, 1);
} }
}); }
} }
// Notify others with general context
if (recipients.length > 0) { if (recipients.length > 0) {
await notificationMongoService.sendToUsers(recipients, { await notificationMongoService.sendToUsers(recipients, {
title: 'New Work Note', title: 'New Work Note',
body: `${user.name || 'A user'} added a note to ${requestId}`, body: `${user.name || 'A user'} added a note to ${workflow.requestNumber}`,
requestId, requestId,
requestNumber: requestId, requestNumber: workflow.requestNumber,
url: `/request/${requestId}`, url: `/request/${workflow.requestNumber}`,
type: 'comment', type: 'comment',
metadata: { noteId: note.noteId } metadata: { noteId: note.noteId }
}); });

View File

@ -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({

39
verify_request.ts Normal file
View File

@ -0,0 +1,39 @@
import mongoose from 'mongoose';
import { WorkflowRequestModel } from './src/models/mongoose/WorkflowRequest.schema';
import { ApprovalLevelModel } from './src/models/mongoose/ApprovalLevel.schema';
import dotenv from 'dotenv';
dotenv.config();
async function verify() {
try {
await mongoose.connect(process.env.MONGO_URI || '');
console.log('Connected to MongoDB');
const requestNumber = 'REQ-2026-02-0010';
const workflow = await WorkflowRequestModel.findOne({ requestNumber });
if (!workflow) {
console.log(`Request ${requestNumber} not found`);
} else {
console.log('Workflow Request found:', {
requestNumber: workflow.requestNumber,
status: workflow.status,
workflowState: workflow.workflowState,
isDraft: workflow.isDraft
});
const levels = await ApprovalLevelModel.find({ requestId: workflow.requestId }).sort({ levelNumber: 1 });
console.log('Approval Levels:');
levels.forEach(l => {
console.log(`Level ${l.levelNumber}: ${l.status}`);
});
}
await mongoose.disconnect();
} catch (err) {
console.error(err);
}
}
verify();