Skip to content

Order Hierarchy

Parent-child order relationships with full lineage tracking, composition metadata, and optimized descendant queries.

Order Hierarchy Features

Complete hierarchical order management:

  • Unlimited Depth - Support complex multi-level workflows (configurable limits)
  • Relationship Validation - Enforce valid parent-child type combinations
  • Lineage Tracking - Maintain root order reference throughout hierarchy
  • Composition Metadata - Track why, when, and how children were created
  • Denormalized Descendants - Fast hierarchy queries without recursion
  • Blocking Control - Children can optionally block or allow parent completion

Use Cases: Multi-stage fulfillment, complex workflows, task decomposition, cross-dock operations

What is Order Hierarchy?

In logistics operations, orders rarely exist in isolation. A single customer order might trigger dozens of internal operations - picking items from shelves, packing boxes, printing labels, arranging transport. The order hierarchy system manages these complex parent-child relationships while maintaining consistency and enforcing business rules.

Why Hierarchies Matter

Traditional Problem: Many systems treat orders as flat, independent entities. This creates challenges:

  • ❌ No visibility into task dependencies
  • ❌ Manual coordination of related orders
  • ❌ Difficult to track "what caused what"
  • ❌ Slow queries to find all related orders
  • ❌ No automatic cleanup on failures

De. Solution: Hierarchical order management with:

  • ✅ Explicit parent-child relationships
  • ✅ Automatic coordination via policies
  • ✅ Full composition metadata
  • ✅ Fast descendant queries
  • ✅ Automatic compensation on failures

Hierarchy Structure

Core Concepts

Root Order The top-level order that initiated the workflow. Could be a customer order, inbound shipment, or cross-dock operation.

Parent Order Any order that spawns child orders. Can be the root or intermediate in a multi-level hierarchy.

Child Order An order created from a parent. Represents a step or task within the parent's workflow.

Descendants All orders below a given order in the hierarchy (children, grandchildren, etc.).

Example Hierarchy

typescript
// E-commerce fulfillment workflow
OutboundOrder (OUT-001) [ROOT]
├── InternalOrder (PICK-001) [CHILD of OUT-001]
│   └── InternalOrder (AUDIT-001) [GRANDCHILD - optional QA]
├── InternalOrder (PACK-001) [CHILD of OUT-001]
│   └── InternalOrder (LABEL-001) [GRANDCHILD - auto-created]
└── ShippingOrder (SHIP-001) [CHILD of OUT-001]
    └── ShippingOrder (RETURN-001) [GRANDCHILD - if delivery failed]

// Hierarchy metadata for OUT-001:
{
  isRoot: true,
  depth: 0,
  childOrders: [PICK-001, PACK-001, SHIP-001],
  allDescendantReferences: [PICK-001, AUDIT-001, PACK-001, LABEL-001, SHIP-001, RETURN-001],
  descendantCount: 6,
  descendantsByType: {
    InternalOrder: [PICK-001, AUDIT-001, PACK-001, LABEL-001],
    ShippingOrder: [SHIP-001, RETURN-001]
  }
}

Hierarchy Schema

Base Hierarchy Fields

Every order contains a hierarchy object:

typescript
{
  // Root tracking
  isRoot: boolean,                    // Is this the top-level order?
  rootOrderId: string,                // ID of root order
  rootOrderReference: string,         // Reference of root order
  rootOrderType: string,              // Type of root order
  
  // Parent tracking
  parentOrderId?: string,             // ID of direct parent
  parentOrderReference?: string,      // Reference of direct parent
  parentOrderType?: string,           // Type of direct parent
  
  // Depth
  depth: number,                      // Level in hierarchy (root = 0)
  
  // Children
  childOrders?: [
    {
      orderId: string,
      orderReference: string,
      orderType: string,
      lspOrderType: string,
      processor: Context,
      status: string,
      
      // WHO/WHAT/WHEN created this child
      created: {
        by: { type, id, name },
        at: { timestamp, timezone }
      },
      
      // WHY was this child created?
      creationReason: 'LIFECYCLE_REQUIREMENT' | 'PIPELINE_STAGE' | 
                      'AUTOMATIC_RULE' | 'CONDITIONAL_TRIGGER' |
                      'EXCEPTION_HANDLING' | 'MANUAL_REQUEST' |
                      'COMPENSATION' | 'SPLIT_ORDER',
      
      // Parent state when child was created
      parentStateAtCreation: {
        process: string,
        stage: string,
        status: string
      },
      
      // Relationship metadata
      relationship: {
        type: 'MANDATORY' | 'OPTIONAL' | 'CONDITIONAL',
        canBlockParent: boolean,     // Can this child block parent completion?
        sequence?: number,            // Order of execution
        dependencies?: string[]       // Other children this depends on
      },
      
      // Additional metadata
      metadata?: {
        trigger?: string,             // What triggered creation
        notes?: string,
        tags?: string[]
      }
    }
  ],
  
  // Denormalized descendants (Flaw #9 fix)
  allDescendantReferences?: string[],        // All descendants (fast queries)
  descendantCount?: number,                  // Total count
  descendantsByType?: Record<string, string[]>  // Grouped by type
}

Relationship Validation

Valid Parent-Child Combinations

Not all order types can spawn all others. The system enforces valid relationships:

Parent TypeAllowed Children
OutboundOrderInternalOrder, ShippingOrder
InboundOrderInternalOrder, PassthroughOrder
InternalOrderInternalOrder (sub-tasks)
ShippingOrderInternalOrder (exceptions), ShippingOrder (returns)
PassthroughOrderSortingOrder, ShippingOrder
SortingOrderShippingOrder, CrossDockOrder

Example:

typescript
// ✅ Valid
OutboundOrder → InternalOrder (picking task)

// ❌ Invalid
InternalOrder → OutboundOrder (tasks can't create customer orders)

Depth Limits

Hierarchies have configurable maximum depth to prevent infinite recursion:

typescript
// Default: 5 levels
ROOT (0) → CHILD (1) → GRANDCHILD (2) → ...MAX_DEPTH (5)

// Exceeding depth throws error
if (childDepth > MAX_DEPTH) {
  return { error: 'Maximum hierarchy depth exceeded' }
}

Composition Metadata

Why Track Composition?

Understanding why and how child orders were created is critical for:

  • Debugging - "Why was this picking task created?"
  • Analytics - "What percentage of tasks are pipeline vs manual?"
  • Auditing - "Who created this exception order?"
  • Optimization - "Which triggers create the most children?"

Creation Reasons

typescript
creationReason: 
  | 'LIFECYCLE_REQUIREMENT'   // Business rule: outbound must have picking
  | 'PIPELINE_STAGE'          // Pipeline auto-created at stage X
  | 'AUTOMATIC_RULE'          // System rule triggered creation
  | 'CONDITIONAL_TRIGGER'     // Condition met (e.g., item requires QA)
  | 'EXCEPTION_HANDLING'      // Created to handle exception
  | 'MANUAL_REQUEST'          // Operator manually created
  | 'COMPENSATION'            // Created as part of rollback
  | 'SPLIT_ORDER'             // Parent split into multiple

Parent State Context

Capture what the parent was doing when child was created:

typescript
parentStateAtCreation: {
  process: 'FULFILLMENT',
  stage: 'PICKING',
  status: 'IN_PROGRESS'
}

// Useful for analysis:
// "Exception orders are mostly created during PACKING stage"
// "Returns happen most often after DELIVERY stage"

Created By

Track who/what created the child:

typescript
created: {
  by: {
    type: 'PIPELINE',                    // Created by automation
    id: 'pipe-outbound-v2',
    name: 'Standard Outbound Pipeline'
  },
  at: { timestamp: 1699123456789, timezone: 'UTC' }
}

// Or manual creation:
created: {
  by: {
    type: 'USER',
    id: 'OPR-123',
    name: 'John Doe'
  },
  at: { timestamp: 1699123456789, timezone: 'America/New_York' }
}

Relationship Types

Mandatory Children

Children that must complete before parent can complete:

typescript
relationship: {
  type: 'MANDATORY',
  canBlockParent: true
}

// Parent completion blocked if child not complete
OutboundOrder cannot complete until:
PICK-001 is COMPLETED
PACK-001 is COMPLETED
SHIP-001 is COMPLETED

Optional Children

Children that don't block parent completion:

typescript
relationship: {
  type: 'OPTIONAL',
  canBlockParent: false
}

// Parent can complete even if optional child pending
OutboundOrder can complete even if:
  ⏸️ AUDIT-001 is still PENDING (quality check)
  ⏸️ PHOTO-001 is still PENDING (documentation)

Use Cases:

  • Quality audits (nice-to-have, not required)
  • Documentation tasks
  • Analytics/reporting tasks
  • Optional gift wrapping

Conditional Children

Children whose blocking status depends on runtime conditions:

typescript
relationship: {
  type: 'CONDITIONAL',
  canBlockParent: item.requiresQA ? true : false
}

// Blocking status determined at creation time
// If item requires QA → child blocks parent
// If item doesn't require QA → child is optional

Sequence & Dependencies

Control execution order and dependencies:

typescript
childOrders: [
  {
    ref: 'PICK-001',
    relationship: {
      type: 'MANDATORY',
      canBlockParent: true,
      sequence: 1,              // Execute first
      dependencies: []          // No dependencies
    }
  },
  {
    ref: 'PACK-001',
    relationship: {
      type: 'MANDATORY',
      canBlockParent: true,
      sequence: 2,              // Execute second
      dependencies: ['PICK-001']  // Wait for picking
    }
  },
  {
    ref: 'SHIP-001',
    relationship: {
      type: 'MANDATORY',
      canBlockParent: true,
      sequence: 3,              // Execute third
      dependencies: ['PACK-001']  // Wait for packing
    }
  }
]

Denormalized Descendants

The Problem

Traditional hierarchy queries require recursion:

typescript
// ❌ Slow: Find all descendants recursively
async function getAllDescendants(orderId) {
  const children = await db.find({ parentOrderId: orderId })
  const descendants = [...children]
  
  for (const child of children) {
    const grandchildren = await getAllDescendants(child.id)  // Recursive!
    descendants.push(...grandchildren)
  }
  
  return descendants
}

// For 3-level hierarchy with 10 orders = 10+ database queries

The Solution

Maintain denormalized lists updated on child creation/removal:

typescript
// ✅ Fast: Single query
const descendants = order.hierarchy.allDescendantReferences
// Returns: ['PICK-001', 'PACK-001', 'LABEL-001', 'SHIP-001', 'RETURN-001']

// Bulk fetch all descendants
const orders = await db.find({ reference: { $in: descendants } })
// 2 queries total instead of 10+

Performance Comparison

OperationBefore (Recursive)After (Denormalized)Speedup
Get all descendants7 queries (3 levels)1 query7x
Bulk fetch descendants7 queries + N fetches2 queries5x
Count descendantsFull traversalRead single field50x+
Filter by typeQuery + filterRead pre-grouped10x

How It Works

On child creation:

typescript
// Update all ancestors with new descendant
await updateAncestorsDescendants(
  db, context, parentOrder, 
  'ADD', 
  childReference, 
  childType
)

// Updates:
// - parent.allDescendantReferences += childReference
// - grandparent.allDescendantReferences += childReference
// - root.allDescendantReferences += childReference
// - All get descendantCount incremented
// - All get descendantsByType[childType] += childReference

On child removal (compensation):

typescript
await updateAncestorsDescendants(
  db, context, parentOrder,
  'REMOVE',
  childReference,
  childType
)

Creating Child Orders

Basic Creation

typescript
import { createChildOrder } from '#lib/order/helpers'

const result = await createChildOrder({
  // Parent info
  parentOrder: outboundOrder,
  parentOrderType: 'OutboundOrder',
  
  // Child info
  childOrderType: 'InternalOrder',
  childOrderId: 'task-123',
  childOrderReference: 'PICK-001',
  lspOrderType: 'InternalOrder',
  processor: context,
  
  // Composition metadata
  creationReason: 'PIPELINE_STAGE',
  created: {
    by: { type: 'PIPELINE', id: 'pipe-001', name: 'Outbound Pipeline' },
    at: { timestamp: Date.now(), timezone: 'UTC' }
  },
  relationship: {
    type: 'MANDATORY',
    canBlockParent: true,
    sequence: 1
  },
  metadata: {
    trigger: 'allocation_complete',
    notes: 'Auto-created picking task'
  }
}, db, context)

// Returns:
{
  hierarchy: { /* child hierarchy object */ },
  tracking: { /* hierarchical tracking numbers */ },
  error: undefined
}

What Happens Automatically

  1. Validates relationship - Checks if parent type can have this child type
  2. Validates depth - Ensures hierarchy doesn't exceed max depth
  3. Generates tracking - Creates hierarchical tracking number
  4. Builds hierarchy - Sets up child hierarchy object with lineage
  5. Updates parent - Adds child to parent's childOrders array
  6. Updates ancestors - Adds child to all ancestors' descendant lists
  7. Returns result - Hierarchy and tracking for the new child

Error Handling

typescript
const result = await createChildOrder(params, db, context)

if (result.error) {
  // Invalid relationship, depth exceeded, etc.
  console.error(result.error)
  // "Invalid order relationship: InternalOrder cannot spawn OutboundOrder"
  // "Maximum order hierarchy depth exceeded. Current depth: 6"
}

Querying Hierarchies

Get All Descendants

typescript
import { getAllDescendants } from '#lib/order/descendants'

// Fast single query
const descendantRefs = await getAllDescendants(db, context, 'OUT-001')
// ['PICK-001', 'PACK-001', 'LABEL-001', 'SHIP-001']

Get Descendant Orders

typescript
import { getAllDescendantOrders } from '#lib/order/descendants'

// Bulk fetch all descendant orders
const descendants = await getAllDescendantOrders(db, context, 'OUT-001')
// Returns full order objects for all descendants

Filter by Type

typescript
import { getDescendantsByType } from '#lib/order/descendants'

// Get only InternalOrders
const taskRefs = await getDescendantsByType(db, context, 'OUT-001', 'InternalOrder')
// ['PICK-001', 'PACK-001', 'LABEL-001']

Direct Access

typescript
// From order object (fastest - no query)
const order = await db.findOne({ reference: 'OUT-001' })

// All descendants
const all = order.hierarchy.allDescendantReferences

// Count
const count = order.hierarchy.descendantCount

// By type
const tasks = order.hierarchy.descendantsByType.InternalOrder
const shipping = order.hierarchy.descendantsByType.ShippingOrder

Best Practices

Design Hierarchies Carefully

✅ Good Hierarchy:

typescript
OutboundOrder
├── InternalOrder (Pick)    // Single picking task
├── InternalOrder (Pack)    // Single packing task
└── ShippingOrder          // Single shipment
// Total: 4 orders, 2 levels

❌ Poor Hierarchy:

typescript
OutboundOrder
├── InternalOrder (Pick item 1)
├── InternalOrder (Pick item 2)
├── InternalOrder (Pick item 3)
// ... 100 items = 100 picking tasks
└── ShippingOrder
// Total: 102 orders, massive overhead

Better Approach:

typescript
OutboundOrder
├── InternalOrder (Pick all items)  // Single task, multiple items
items: [item1, item2, item3, ...]
└── ShippingOrder
// Total: 3 orders, efficient

Use Relationship Types Wisely

Mandatory for critical paths:

typescript
// Parent cannot complete without these
relationship: { type: 'MANDATORY', canBlockParent: true }

Optional for nice-to-haves:

typescript
// Parent can complete even if pending
relationship: { type: 'OPTIONAL', canBlockParent: false }

Set Meaningful Metadata

❌ Generic:

typescript
metadata: { notes: 'Task created' }

✅ Specific:

typescript
metadata: {
  trigger: 'inventory_allocated',
  notes: 'Auto-created after allocation for SKU-12345',
  tags: ['auto-created', 'high-priority']
}

Leverage Denormalized Queries

❌ Don't query recursively:

typescript
// Slow - multiple queries
async function getAll(id) {
  const children = await db.find({ parentOrderId: id })
  for (const child of children) {
    await getAll(child.id)  // Recursion!
  }
}

✅ Use denormalized lists:

typescript
// Fast - single query
const descendants = await getAllDescendants(db, context, orderId)

Use Cases

Multi-Stage Fulfillment

typescript
OutboundOrder (Customer order)
├── InternalOrder (Allocate inventory)
├── InternalOrder (Pick items)
│   └── InternalOrder (Quality check) [OPTIONAL]
├── InternalOrder (Pack box)
│   └── InternalOrder (Gift wrap) [OPTIONAL]
├── InternalOrder (Print label)
└── ShippingOrder (Last-mile delivery)
    └── ShippingOrder (Return) [Created if delivery fails]

Cross-Dock Operations

typescript
InboundOrder (Receiving at hub)
└── PassthroughOrder (No storage)
    ├── SortingOrder (Sort by destination)
    └── ShippingOrder (Route to next hub)
        └── ShippingOrder (Final delivery)

Exception Handling

typescript
OutboundOrder (Normal flow)
├── InternalOrder (Pick)
│   └── InternalOrder (Exception - damaged item found)
│       └── InternalOrder (Re-pick replacement item)
├── InternalOrder (Pack)
└── ShippingOrder

Next Steps


Related Documentation: