Back to ER Diagram
Approval Logic

Approval Workflow Logic

Complete documentation of multi-level approval matrix, delegation, auto-approval, escalation, and entity-specific approval workflows for Purchase Requests, RFQs, POs, Invoices, and Vendors.

8 PG Tables + 1 MongoDB Collection
Schema v3.0
Approval + Entity Modules

System Overview

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.

config.approval_matrix
config.approval_levels
pr.pr_approvals
rfq.rfq_approvals
po.po_approvals
logistics.invoice_approvals
vendor.vendor_approvals
config.approval_delegation

Tables Involved

config.approval_matrix

  • Defines approval rules per entity type and amount range
  • Columns: document_type, document_sub_type, company_id, department_id, priority, effective_from/until
  • document_sub_type: PR→CAPITAL/REVENUE/MRO/PROJECT, RFQ→OPEN/LIMITED/SINGLE_SOURCE, PO→STANDARD/BLANKET/EMERGENCY
  • Amount thresholds defined at approval_levels (min_amount, max_amount per level)
  • Example: PR type CAPITAL for IT Dept with amount 50K-100K requires 3-level approval

config.approval_levels

  • Details each level within a matrix entry
  • Columns: matrix_id, level_number, level_name, min_amount, max_amount, approver_type, approver_user_id, approver_role_id, department_id
  • approver_type: USER, ROLE, DEPARTMENT_HEAD, MANAGER, COST_CENTER_OWNER
  • can_skip_if_requestor: skip this level if approver is the requestor
  • is_parallel + require_all_parallel: multiple approvers at same level (any/all must approve)
  • auto_approve_days: auto-approve if no action within X days
  • escalation_days + escalation_to_id: escalate to another user after N days of inaction

config.approval_delegation

  • Temporary delegation when approver is unavailable
  • Valid within date range (valid_from, valid_until)
  • Can be entity-type specific or global

Entity Approval Tables

  • pr.pr_approvals - Purchase Request approvals
  • rfq.rfq_approvals - RFQ approvals
  • po.po_approvals - Purchase Order approvals
  • logistics.invoice_approvals - Invoice approvals
  • vendor.vendor_approvals - Vendor registration approvals

Multi-Level Approval Chain

Example: 3-level approval for a Purchase Order worth $75,000

L1
Dept Manager
L2
Finance Head
L3
Director
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 - -

Approval Workflow Steps

1

Determine Applicable Matrix

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;
2

Create Approval Records

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;
3

Check for Delegation

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;
4

Process Approval/Rejection

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;
5

Update Entity Status

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;

Approval Matrix Configuration

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 -

Special Features

Auto-Approval

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.

Skip Level

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.

Delegation

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.

Rejection & Re-submission Workflow

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.

L1 Approved
L2 Approved
L3 Rejects
Entity → REJECTED
Notify Originator
Originator Revises → REVISION_REQUESTED / DRAFT
Re-submit → Fresh Approval Cycle

Re-submission Rules

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

Vendor Comments During RFQ Award Approval

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.

MongoDB: comments_threads

  • entity_type: "RFQ_AWARD" (or "RFQ", "PO", etc.)
  • entity_id: The RFQ ID being awarded
  • context: JSONB with vendor_id to scope comments per vendor
  • user_id + user_display_name: Buyer or Approver who commented
  • parent_comment_id: Enables threaded replies
  • comment_type: COMMENT, RECOMMENDATION, CONCERN, QUESTION
  • mentioned_users: Tag approvers for attention
  • is_internal: true (not visible to vendors)

How It Works

When 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.

API Implementation (.NET)

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

Entity-Specific Approval Tables

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