From 6c0fac7e0dc927794920864b2aed76fdc4a54d94 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 6 Feb 2026 15:54:16 +0530 Subject: [PATCH] additional approver error due to step level mismatch fixed --- docs/UNIFIED_REQUEST_ARCHITECTURE.md | 35 ++++++++- src/routes/workflow.routes.ts | 2 +- src/services/workflow.service.ts | 104 ++++++++++++++++++++++++++- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/docs/UNIFIED_REQUEST_ARCHITECTURE.md b/docs/UNIFIED_REQUEST_ARCHITECTURE.md index b20a93c..12980eb 100644 --- a/docs/UNIFIED_REQUEST_ARCHITECTURE.md +++ b/docs/UNIFIED_REQUEST_ARCHITECTURE.md @@ -60,7 +60,40 @@ The user's requirement to track previous proposals during resubmission is handle - Overwrites the main object with the *new* data. - Advances the workflow. -### 4. Implementation Strategy +### 4. KPI & Deep Filtering Strategy (Hybrid Approach) + +To support complex KPIs and high-performance filtering across thousands of requests, we use a **Referential Flat Pattern**: + +- **Workflow Index (Speed)**: `WorkflowRequest` remains light. It handles high-frequency queries like "My Pending Tasks" or "Recent Activity". +- **Business Index (Depth)**: `DealerClaim` holds the "Deep Data". We apply Mongoose/MongoDB indexes on fields like: + - `dealer.region`, `dealer.state` (for Geospatial/Regional KPIs). + - `budgetTracking.utilizedBudget` (for Financial KPIs). + - `completion.expenses.category` (for operational analysis). + +**The "Hybrid" Advantage:** +1. **Performance**: We don't bloat the main `workflow_requests` collection with hundreds of dealer-specific fields. This keeps "Total Request" counts and general listing extremely fast. +2. **Scalability**: For deep filters (e.g., "Show all claims in South Region with expenses > 50k"), we query the `dealer_claims` collection first to get the `requestId`s, then fetch the workflow status. This is much faster than a massive `$lookup` on a single bloated collection. +3. **Clean KPIs**: KPIs like "Budget vs Actual" are calculated directly from `DealerClaim` without interfering with generic workflow TAT metrics. + +### 5. Ad-Hoc & Additional Approver Handling + +When a user manually adds an approver (Ad-hoc) to a Dealer Claim or Custom Flow: +- **Tag Assignment**: The new level is automatically tagged with `stepAction: 'NONE'` and `stepPersona: 'ADDITIONAL_APPROVER'`. +- **UI Consistency**: The frontend sees `stepAction: 'NONE'` and renders the standard approval interface (comments + buttons). +- **Rejection Intelligence**: + - If an *Additional Approver* rejects, the system looks back for the nearest **anchor step** (e.g., `stepAction: 'DEALER_PROPOSAL'`). + - This prevents the workflow from getting "stuck" between two manually added levels if the business rule requires a return to the initiator or dealer. + +### 6. Impact on Custom Flows & Compatibility + +**Zero Breaking Changes**: +- Existing Custom Flows will default to `stepAction: 'NONE'`. The UI behavior remains identical to the current state. +- The `WorkflowRequest` collection structure is not being modified; we are only adding two optional metadata fields to the `ApprovalLevel` sub-documents. + +**Future-Proofing**: +- Custom Flows can now "unlock" specialized steps (like `PROPOSAL_EVALUATION`) simply by updating their template metadata, without any backend code changes. + +### 7. Implementation Strategy | Feature | Custom Request Path | Dealer Claim Path | | :--- | :--- | :--- | diff --git a/src/routes/workflow.routes.ts b/src/routes/workflow.routes.ts index e0eaf5a..4f7ba93 100644 --- a/src/routes/workflow.routes.ts +++ b/src/routes/workflow.routes.ts @@ -875,8 +875,8 @@ router.post('/:id/approvers/at-level', const result = await workflowService.addApproverAtLevel( requestId, email, - Number(tatHours), Number(level), + Number(tatHours), req.user?.userId ); diff --git a/src/services/workflow.service.ts b/src/services/workflow.service.ts index e3dfdbe..426c8fc 100644 --- a/src/services/workflow.service.ts +++ b/src/services/workflow.service.ts @@ -862,6 +862,56 @@ export class WorkflowServiceMongo { // Update workflow totalLevels and totalTatHours request.totalLevels = targetLevel; request.totalTatHours += tatHours; + + // Handle reactivation if adding a level to a finished workflow + if (request.status === 'APPROVED' || request.workflowState === 'CLOSED') { + // Update request to point to this new level as current + request.currentLevel = targetLevel; + request.currentLevelId = newLevel.levelId; + + // Reactivate workflow + request.status = 'PENDING'; + request.workflowState = 'OPEN'; + request.closureDate = undefined; + request.conclusionRemark = undefined; + + // Activate the new level's TAT + const now = new Date(); + const priority = (request.priority || 'STANDARD').toLowerCase(); + const endTime = priority === 'express' + ? (await addWorkingHoursExpress(now, tatHours)).toDate() + : (await addWorkingHours(now, tatHours)).toDate(); + + newLevel.status = 'PENDING'; + newLevel.tat.startTime = now; + newLevel.tat.endTime = endTime; + await newLevel.save(); + + // Schedule TAT for the new approver + await tatScheduler.scheduleTatJobs( + request.requestId, + newLevel._id.toString(), + user.userId, + tatHours, + now, + request.priority as any + ); + + // Send Notification to the new approver + await notificationMongoService.sendToUsers([user.userId], { + title: 'New Request Assigned (Ad-hoc Reactivated)', + body: `Workflow has been reactivated. You have a new request ${request.requestNumber} pending your approval at level ${targetLevel}.`, + type: 'assignment', + requestId: request.requestId, + requestNumber: request.requestNumber, + priority: request.priority as any + }); + } else if (targetLevel === request.currentLevel + 1 && request.status === 'PENDING') { + // If we just added the next level to a pending request, we don't necessarily activate it yet + // because the current level is still pending. + // But we should update totalLevels which we already did. + } + await request.save(); // Add as participant @@ -883,10 +933,14 @@ export class WorkflowServiceMongo { } else { // Case 2: Level exists - Shift existing approver to next level - if (existingLevel.status === 'APPROVED' || existingLevel.status === 'SKIPPED') { - throw new Error('Cannot modify completed level'); + // APPROVED levels should not be modified to preserve audit trail + if (existingLevel.status === 'APPROVED') { + throw new Error('Cannot modify already approved level'); } + // If level was SKIPPED, we effectively "un-skip" this slot by inserting a new one + // and shifting the skipped one down. + // Get all levels at or after the target level const levelsToShift = await ApprovalLevelModel.find({ requestId: request.requestId, @@ -930,6 +984,52 @@ export class WorkflowServiceMongo { // Update workflow totalLevels and totalTatHours request.totalLevels += 1; request.totalTatHours += tatHours; + + // Handle progress update if inserting at or before current level OR if workflow was already finished + if (targetLevel <= request.currentLevel || request.status === 'APPROVED' || request.workflowState === 'CLOSED') { + // Update request to point to this new level as current + request.currentLevel = targetLevel; + request.currentLevelId = newLevel.levelId; + + // Reactivate workflow if it was closed + request.status = 'PENDING'; + request.workflowState = 'OPEN'; + request.closureDate = undefined; + request.conclusionRemark = undefined; + + // Activate the new level's TAT + const now = new Date(); + const priority = (request.priority || 'STANDARD').toLowerCase(); + const endTime = priority === 'express' + ? (await addWorkingHoursExpress(now, tatHours)).toDate() + : (await addWorkingHours(now, tatHours)).toDate(); + + newLevel.status = 'PENDING'; + newLevel.tat.startTime = now; + newLevel.tat.endTime = endTime; + await newLevel.save(); + + // Schedule TAT for the new approver + await tatScheduler.scheduleTatJobs( + request.requestId, + newLevel._id.toString(), + user.userId, + tatHours, + now, + request.priority as any + ); + + // Send Notification to the new approver + await notificationMongoService.sendToUsers([user.userId], { + title: 'New Request Assigned (Ad-hoc Reactivated)', + body: `Workflow has been reactivated. You have a new request ${request.requestNumber} pending your approval at level ${targetLevel}.`, + type: 'assignment', + requestId: request.requestId, + requestNumber: request.requestNumber, + priority: request.priority as any + }); + } + await request.save(); // Add as participant