Blog

How I Implemented Department-Scoped Approval Workflows for Requisitions

Amount thresholds, manager vs finance gates, and department matching in a pure TypeScript workflow engine for CPMS.

How I Implemented Department-Scoped Approval Workflows for Requisitions
PMS1 min read2026-04-03
By Published Updated

How I Implemented Department-Scoped Approval Workflows for Requisitions

Direct answer (40 words): Split “what status comes next” from “who may click approve”: threshold rules move PENDING_MANAGER to APPROVED or PENDING_FINANCE by amount; who may approve uses department manager id, approver department match, and finance permission—so approvals follow org structure, not only role names.

Status machine (amount-driven)

const MANAGER_ONLY_THRESHOLD = new Decimal(5000);

/**
 * Determine the next status for a requisition after an approval action.
 *
 * Workflow rules:
 * - DRAFT -> submit -> PENDING_MANAGER
 * - PENDING_MANAGER + Manager approves + amount <= 5000 -> APPROVED
 * - PENDING_MANAGER + Manager approves + amount >  5000 -> PENDING_FINANCE
 * - PENDING_FINANCE + Finance approves -> APPROVED
 */
export function determineNextStatus(
  totalAmount: Decimal,
  currentStatus: RequisitionStatus
): RequisitionStatus {
  switch (currentStatus) {
    case "DRAFT":
      return "PENDING_MANAGER";

    case "PENDING_MANAGER":
      if (totalAmount.lte(MANAGER_ONLY_THRESHOLD)) {
        return "APPROVED";
      }
      return "PENDING_FINANCE";

    case "PENDING_FINANCE":
      return "APPROVED";

    default:
      return currentStatus;
  }
}

Authorization (org context)

export function canUserApprove(
  userId: string,
  userPermissions: Permissions | null | undefined,
  userDepartmentId: string | null,
  requisition: RequisitionForAuth
): boolean {
  if (requisition.status === "PENDING_MANAGER") {
    const isDeptManager = requisition.department.managerId === userId;
    const isApproverInDept =
      canApproveManager(userPermissions) &&
      userDepartmentId === requisition.departmentId;
    return isDeptManager || isApproverInDept;
  }

  if (requisition.status === "PENDING_FINANCE") {
    return canApproveFinance(userPermissions);
  }

  return false;
}

Why this design scales

  • Thresholds are easy to tune per deployment (policy, currency, or risk appetite).
  • Department scoping prevents cross-department approval spoofing when multiple buyers share a role label.
  • Pure functions stay testable without mocking Prisma—ideal for unit tests around edge cases.

Read the full case study: CPMS — Comprehensive Procurement Management System

Related reading

Why I Used Prisma Client Extensions for CRUD Notifications (MSIS Perspective)

Cross-cutting notification emission on create/update/delete without breaking mutations, plus permission-aware recipients in CPMS.

Continue reading

Why I Used JSON-Path RBAC and JWT Session Refresh in Auth.js v5 (Next.js 15)

Nested permission JSON, dot-path checks, legacy role fallbacks, and inactivity-aware JWT callbacks for a procurement CPMS.

Continue reading

How I Implemented Secure S3 Presigned URLs and Server Uploads for Procurement Documents

S3-compatible uploads with sanitized keys, server-side PutObject, and presigned GET for invoices and supplier documents in a Next.js App Router CPMS.

Continue reading