additional approver error due to step level mismatch fixed

This commit is contained in:
laxmanhalaki 2026-02-06 15:54:16 +05:30
parent 5b9012d314
commit 6c0fac7e0d
3 changed files with 137 additions and 4 deletions

View File

@ -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 |
| :--- | :--- | :--- |

View File

@ -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
);

View File

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