Procurement purchase-order

Purchase Order Routing

IT, Operations, and Marketing each have their own approval chain. Enterprise software requires Security sign-off. Finance steps in above $10k. Split PO detection catches attempts to circumvent approval thresholds.

IT Procurement
Manager
→
IT Director
→
if enterpriseSecurity
→
if > $10kFinance
Marketing
Manager
→
if > $5kMkt Director
→
if > $10kFinance
Operations
Manager
→
Ops Head
→
if > $10kFinance

Department-aware chain

A single purchase-order template handles all three departments. The base chain is the manager; conditions add levels based on data.department and data.amount.

  • IT always routes through IT Director
  • Enterprise contracts add Security sign-off
  • Marketing only adds director above $5k
  • Finance is a universal gate above $10k
  • Other departments need their own condition
po-template.ts
engine.registerResolver('direct-manager', (submittedBy) => hrSystem.managerIdOf(submittedBy));

const department = (name: string) => ({ field: 'department', operator: '==', value: name });

await engine.defineTemplate({
  name: 'purchase-order',
  documentType: 'purchase-order',
  allowOverride: true, // required for engine.override() below
  levels: [
    {
      level: 1,
      name: 'Manager',
      approvers: [{ type: 'dynamic', resolver: 'direct-manager' }],
      mode: 'any',
    },
  ],
  conditions: [
    {
      when: department('it'),
      addLevels: [{ level: 2, name: 'IT director', approvers: [{ type: 'role', role: 'it-director' }], mode: 'any' }],
    },
    // Enterprise software also needs Security sign-off
    {
      when: { all: [department('it'), { field: 'isEnterpriseContract', operator: '==', value: true }] },
      addLevels: [{ level: 3, name: 'Security', approvers: [{ type: 'role', role: 'security-officer' }], mode: 'any' }],
    },
    {
      when: { all: [department('marketing'), { field: 'amount', operator: '>', value: 5_000 }] },
      addLevels: [{ level: 2, name: 'Marketing director', approvers: [{ type: 'role', role: 'marketing-director' }], mode: 'any' }],
    },
    {
      when: department('operations'),
      addLevels: [{ level: 2, name: 'Operations head', approvers: [{ type: 'role', role: 'operations-head' }], mode: 'any' }],
    },
    // Finance is a universal gate above $10,000
    {
      when: { field: 'amount', operator: '>', value: 10_000 },
      addLevels: [{ level: 4, name: 'Finance', approvers: [{ type: 'role', role: 'finance-controller' }], mode: 'any' }],
    },
  ],
});

Three scenarios that break hand-rolled PO systems.

Each one involves state coordination that a simple if/else can't handle safely.

01

Split PO detection

An employee submits a $9,999 PO on Monday and a $8,000 PO to the same vendor on Wednesday — both under the $10k Finance threshold. Combined: $17,999.

split-po.ts
// Sum the vendor's open POs from the same requester over the last 30 days
async function openTotalForVendor(vendorId: string, submittedBy: string): Promise<number> {
  const { items } = await engine.queryInstances(
    {
      documentType: 'purchase-order',
      status: 'pending',
      submittedBy,
      fromDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
      data: { vendorId },
    },
    { limit: 500, offset: 0 }
  );
  return items.reduce((total, po) => total + Number(po.data.amount), 0);
}

// Call before engine.submit()
const combined = (await openTotalForVendor(vendorId, userId)) + amount;
if (amount <= 10_000 && combined > 10_000) {
  throw new Error(`Split PO detected: combined total ${combined}`);
}

Run the check in your API before calling engine.submit(). queryInstances matches data fields by equality, so the amount is summed in your code.

02

Emergency procurement override

A production server fails at 2am. IT needs to order a replacement immediately. The normal 48-hour approval cycle isn't viable.

override.ts
// Emergency procurement: bypass the remaining levels
const instance = await engine.override(poInstance.id, {
  overriddenBy: 'admin-cso',
  justification: 'Emergency server replacement - P0 incident, board notified',
});
// instance.status === 'approved'

// The audit trail gets an 'overridden' entry:
// { action: 'overridden', actorId: 'admin-cso', reason: 'Emergency server replacement ...', ... }

override() only works on templates with allowOverride: true. Restrict who may call it with an authorizationPolicy (the operation is 'override'). The justification is stored on the audit entry.

03

Partial line-item rejection

Finance approves the hardware line but rejects the 3-year extended warranty. Approval is per document, so the requester revises the PO and resubmits it.

resubmit.ts
await engine.reject(instance.id, {
  approverId: 'finance-fiona',
  reason: 'Hardware approved; remove the 3-year extended warranty line',
});

// The requester fixes the data and resubmits
const revised = await engine.resubmit(instance.id, {
  resubmittedBy: 'emp-0712',
  reason: 'Warranty line removed',
  updatedData: { amount: 7_000 },
});
// revised.status === 'pending', revised.parentInstanceId === instance.id

The rejected instance stays in the audit trail as status: rejected. resubmit() creates a new instance from level 1 and links it through parentInstanceId.

Your next approval workflow
ships faster than the last one.

Drop it into any Node.js project. No database required for local development.

$ npm install hierarchical-approval

Peer dep for Postgres: npm install pg @types/pg