additional approver error due to step level mismatch fixed
This commit is contained in:
parent
5b9012d314
commit
6c0fac7e0d
@ -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 |
|
||||
| :--- | :--- | :--- |
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user