Complete documentation of multi-level approval matrix, delegation, auto-approval, escalation, and entity-specific approval workflows for Purchase Requests, RFQs, POs, Invoices, and Vendors.
The ProKure approval system uses a configurable matrix that determines approval levels based on entity type, department, and amount thresholds. Each entity (PR, RFQ, PO, Invoice, Vendor) has its own approval tracking table.
pr.pr_approvals - Purchase Request approvalsrfq.rfq_approvals - RFQ approvalspo.po_approvals - Purchase Order approvalslogistics.invoice_approvals - Invoice approvalsvendor.vendor_approvals - Vendor registration approvalsExample: 3-level approval for a Purchase Order worth $75,000
| Level | Approver | Status | Approved At | Remarks |
|---|---|---|---|---|
| Level 1 | John Smith (Dept Manager) | Approved | 2026-01-10 09:30 | Approved as per requirement |
| Level 2 | Sarah Johnson (Finance Head) | Approved | 2026-01-10 14:15 | Budget verified |
| Level 3 | Michael Chen (Director) | Pending | - | - |
When an entity is submitted for approval, find the matching approval matrix based on entity type, company, department, and amount.
SELECT am.*, al.* FROM approval.approval_matrix am JOIN approval.approval_levels al ON al.matrix_id = am.id WHERE am.document_type = @documentType -- 'PR', 'PO', 'RFQ', etc. AND (am.document_sub_type = @subType OR am.document_sub_type IS NULL) -- 'CAPITAL', 'REVENUE', etc. AND am.company_id = @companyId AND (al.department_id = @departmentId OR al.department_id IS NULL) AND al.min_amount <= @amount AND al.max_amount >= @amount AND am.is_active = true AND (am.effective_from IS NULL OR am.effective_from <= CURRENT_DATE) AND (am.effective_until IS NULL OR am.effective_until >= CURRENT_DATE) ORDER BY am.priority DESC, al.level_number;
Create pending approval entries for each level in the entity's approval table.
-- Example for PO; same pattern for PR, RFQ, Invoice, Vendor INSERT INTO approval.vendor_approvals (vendor_id, approval_level, approver_id, is_current_level) SELECT @entityId, al.level_number, COALESCE(al.approver_user_id, GetUserByRole(al.approver_role_id, @departmentId)), (al.level_number = 1) -- Only L1 is current initially FROM approval.approval_levels al WHERE al.matrix_id = @matrixId ORDER BY al.level_number;
Before notifying the approver, check if they have delegated their approval authority.
SELECT delegate_id FROM config.approval_delegation WHERE delegator_id = @approverId AND (entity_type = 'PO' OR entity_type IS NULL) AND valid_from <= NOW() AND valid_until >= NOW() AND is_active = true;
When an approver takes action, update the approval record and check if all levels are complete.
-- Update approval record UPDATE po.po_approvals SET status = @status, -- 'APPROVED' or 'REJECTED' remarks = @remarks, approved_at = NOW() WHERE po_id = @poId AND approval_level = @currentLevel; -- Check if all levels approved SELECT CASE WHEN COUNT(*) FILTER(WHERE status = 'REJECTED') > 0 THEN 'REJECTED' WHEN COUNT(*) FILTER(WHERE status = 'PENDING') = 0 THEN 'APPROVED' ELSE 'PENDING' END AS final_status FROM po.po_approvals WHERE po_id = @poId;
Update the main entity's status based on approval outcome.
UPDATE po.purchase_orders SET status = CASE WHEN @finalStatus = 'APPROVED' THEN 'APPROVED' WHEN @finalStatus = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING_APPROVAL' END WHERE id = @poId;
Example configuration showing different approval levels based on amount thresholds:
| Document Type | Sub-Type | Amount Range | Levels | Level 1 | Level 2 | Level 3 |
|---|---|---|---|---|---|---|
PR |
CAPITAL |
$0 - $50,000 | 2 | Dept Manager | Finance Head | - |
PR |
CAPITAL |
$50,001+ | 3 | Dept Manager | Finance Head | Director |
PR |
REVENUE |
$0 - $10,000 | 1 | Dept Manager | - | - |
PR |
REVENUE |
$10,001+ | 2 | Dept Manager | Finance Head | - |
RFQ |
OPEN |
$0 - $50,000 | 2 | Purchase Manager | Finance Head | - |
RFQ |
OPEN |
$50,001+ | 3 | Purchase Manager | Finance Head | Director |
PO |
STANDARD |
$0 - $10,000 | 1 | Dept Manager | - | - |
PO |
STANDARD |
$10,001 - $50,000 | 2 | Dept Manager | Finance Head | - |
PO |
STANDARD |
$50,001 - $100,000 | 3 | Dept Manager | Finance Head | Director |
PO |
EMERGENCY |
Any | 2 | Dept Manager | Director | - |
VENDOR |
- | Any | 2 | Purchase Manager | Compliance Officer | - |
If auto_approve_days is set on an approval level, a scheduled job automatically approves the request if no action is taken within the specified days. This prevents bottlenecks from unavailable approvers.
If can_skip is true and a higher-level approver (e.g., Director) approves directly, lower pending levels are automatically marked as skipped. This allows senior management to fast-track urgent approvals.
Approvers can delegate their authority to another user for a specific date range. When checking for pending approvals, both the original approver and delegate are notified. Either can approve.
When any approver in the chain rejects, the entity returns to the originator for revision. This covers the eProc scenario where a later-level approver rejects after earlier levels have already approved.
On rejection: (1) Entity status set to REJECTED, (2) All existing approval records are preserved for audit trail (action_date frozen), (3) Originator is notified with rejection remarks, (4) Originator modifies the entity and re-submits, (5) A new set of approval records is created (previous cycle records remain for history), (6) The approval chain re-starts from Level 1. Previous approvals do NOT carry forward — this prevents rubber-stamping after material changes.
-- On rejection: update entity status UPDATE procurement.purchase_requests SET status = 'REJECTED', rejection_count = rejection_count + 1, last_rejected_at = NOW(), last_rejected_by = @rejecterId, updated_at = NOW() WHERE id = @prId; -- On re-submission after revision: -- 1. Entity status → PENDING_APPROVAL -- 2. Create fresh approval records (new cycle) -- 3. Previous approval records kept with approval_cycle number for audit
During RFQ award approval, buyers and approvers can add per-vendor comments and recommendations. This is handled through the MongoDB comments_threads collection rather than a flat remarks field.
vendor_id to scope comments per vendorWhen a buyer submits an RFQ award for approval, they attach per-vendor comments (justification, pricing notes, risk flags). Each approver in the chain can view all vendor comments, the bid comparison data (via the RFQ entity), and add their own remarks per vendor. The approval record's comments field captures the overall approval/rejection remark, while detailed vendor-specific commentary lives in comments_threads.
public class ApprovalService { public async Task SubmitForApprovalAsync(string entityType, long entityId, decimal amount) { // 1. Get applicable matrix var matrix = await GetApprovalMatrixAsync(entityType, amount); // 2. Get approval levels var levels = await _context.ApprovalLevels .Where(l => l.MatrixId == matrix.Id) .OrderBy(l => l.LevelNumber) .ToListAsync(); // 3. Create approval records foreach (var level in levels) { var approverId = await ResolveApprover(level); var delegateId = await CheckDelegation(approverId, entityType); await CreateApprovalRecord(entityType, entityId, level.LevelNumber, delegateId ?? approverId); } // 4. Notify first level approver await NotifyApprover(entityType, entityId, 1); } public async Task ProcessApprovalAsync(string entityType, long entityId, int level, bool approved, string remarks) { // 1. Update approval record await UpdateApprovalRecord(entityType, entityId, level, approved, remarks); // 2. Check final status var status = await CalculateFinalStatus(entityType, entityId); // 3. Update entity await UpdateEntityStatus(entityType, entityId, status); // 4. Notify next level or complete if (status == "PENDING" && approved) await NotifyApprover(entityType, entityId, level + 1); else await NotifyOriginator(entityType, entityId, status); } }
Each entity type has its own approval tracking table with similar structure:
| Entity | Approval Table | Foreign Key | Common Columns |
|---|---|---|---|
| Purchase Request | pr.pr_approvals |
pr_id | approval_level, approver_id, status, remarks, approved_at |
| RFQ | rfq.rfq_approvals |
rfq_id | |
| Purchase Order | po.po_approvals |
po_id | |
| Invoice | logistics.invoice_approvals |
invoice_id | |
| Vendor | vendor.vendor_approvals |
vendor_id |