Skip to content

Lifecycle Policies & Validation

Business rule enforcement for order state transitions - ensure parent-child consistency, prevent invalid operations, and maintain data integrity.

Lifecycle Validation

Comprehensive validation layer for order operations:

  • Completion Policies - Control when orders can be marked complete
  • Cancellation Policies - Define rules for order cancellation
  • Parent-Child Coordination - Automatic status synchronization
  • Blocking Control - Children can block or allow parent operations
  • Validation Helpers - Reusable functions for consistent enforcement
  • Error Prevention - Stop invalid operations before they happen

Enforced by: Both manual operations and pipeline automation use the same validation layer.

What are Lifecycle Policies?

Lifecycle policies define the business rules that govern how orders move through their operational lifecycle. They answer critical questions like:

  • Can a parent order complete if children are still pending?
  • What happens to child orders when a parent is cancelled?
  • Can a child's completion automatically trigger parent completion?
  • Which child statuses are acceptable for parent completion?

Why Lifecycle Validation Matters

Without Validation:

typescript
// ❌ Problems without policies
await db.updateOne({ ref: 'OUT-001' }, { status: 'COMPLETED' })

// Issues:
// - Parent marked complete but picking tasks still pending
// - Data inconsistency between parent and children
// - No audit trail of what was validated
// - Manual cleanup when errors discovered later
// - Customer sees "delivered" but order still in warehouse

With Validation:

typescript
// ✅ Validated completion
const validation = await canCompleteOrder(order, orderType, db, context)

if (!validation.allowed) {
  return {
    error: true,
    reason: validation.reason,
    blockers: validation.blockers
    // "Cannot complete: 2 child orders not in required status"
    // blockers: [{ ref: 'PICK-001', status: 'PENDING', required: ['COMPLETED'] }]
  }
}

// Safe to complete - all validations passed
await db.updateOne({ ref: 'OUT-001' }, { status: 'COMPLETED' })

Lifecycle Policy Schema

Policy Structure

Every order type can have a lifecycle policy:

typescript
{
  // Order type this policy applies to
  orderType: 'OutboundOrder',
  
  // Parent completion rules
  parentCompletion: {
    requireAllChildrenComplete: boolean,
    allowCompletionIfChildrenAre?: string[],  // Acceptable child statuses
    blockingChildren?: string[]               // Child types that can block
  },
  
  // Child completion rules  
  childCompletion: {
    autoCompleteParent: boolean,              // Complete parent when all children done?
    notifyParent: boolean,                    // Update parent on child completion?
    updateParentStatus?: string               // Status to set on parent
  },
  
  // Parent cancellation rules
  parentCancellation: {
    allowWithActiveChildren: boolean,
    childAction: 'CASCADE_CANCEL' | 'ORPHAN' | 'COMPLETE_FIRST' | 'SUSPEND',
    requiresApproval?: boolean
  },
  
  // Child cancellation rules
  childCancellation: {
    notifyParent: boolean,
    triggerParentAction?: 'CANCEL' | 'MARK_FAILED' | 'NONE'
  }
}

Completion Validation

Parent Completion Rules

Control when a parent order can be marked complete:

Example: OutboundOrder Policy

typescript
{
  orderType: 'OutboundOrder',
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED', 'DELIVERED'],
    blockingChildren: ['InternalOrder', 'ShippingOrder']
  }
}

What this means:

  • ✅ Parent can complete if all children are COMPLETED or DELIVERED
  • ❌ Parent cannot complete if any child is PENDING, IN_PROGRESS, etc.
  • ✅ Only InternalOrder and ShippingOrder children are checked (others ignored)

Validation Example:

typescript
OutboundOrder (OUT-001) - trying to complete
├── InternalOrder (PICK-001) - COMPLETED
├── InternalOrder (PACK-001) - IN_PROGRESS
└── ShippingOrder (SHIP-001) - PENDING

// Validation result:
{
  allowed: false,
  reason: "Cannot complete: 2 child orders not in required status",
  blockers: [
    { ref: 'PACK-001', type: 'InternalOrder', status: 'IN_PROGRESS', required: ['COMPLETED', 'DELIVERED'] },
    { ref: 'SHIP-001', type: 'ShippingOrder', status: 'PENDING', required: ['COMPLETED', 'DELIVERED'] }
  ]
}

Root Order Exception

Root orders without children can always complete:

typescript
// Root order with no children
OutboundOrder (OUT-001) 
// No childOrders array or empty array

// ✅ Can always complete - no children to validate

Why? If an order has no children, there's nothing to validate. This is common for simple orders that don't require task decomposition.

Blocking Control with Relationships

Children can opt out of blocking parent completion:

typescript
OutboundOrder (OUT-001)
├── InternalOrder (PICK-001) 
relationship: { type: 'MANDATORY', canBlockParent: true }
// ✅ Must be complete before parent completes

├── InternalOrder (PACK-001)
relationship: { type: 'MANDATORY', canBlockParent: true }
// ✅ Must be complete before parent completes

└── InternalOrder (AUDIT-001)
    relationship: { type: 'OPTIONAL', canBlockParent: false }
    // ⏸️ Can be PENDING - won't block parent

Validation Logic:

typescript
for (const child of children) {
  const canBlockParent = child.relationship?.canBlockParent ?? true  // Default: blocking
  
  if (!canBlockParent) {
    continue  // Skip this child - doesn't block
  }
  
  // Check if child status is acceptable
  if (!policy.parentCompletion.allowCompletionIfChildrenAre.includes(child.status)) {
    blockers.push(child)
  }
}

Result:

typescript
// OUT-001 can complete when:
// - PICK-001 is COMPLETED ✅
// - PACK-001 is COMPLETED ✅
// - AUDIT-001 can be any status (doesn't matter) ⏸️

Child Auto-Completion

Child completion can automatically trigger parent completion:

typescript
{
  orderType: 'InternalOrder',
  childCompletion: {
    autoCompleteParent: true,
    notifyParent: true
  }
}

How it works:

  1. Child order completes
  2. System checks if all sibling children are complete
  3. If yes, automatically completes parent
  4. Parent's completion triggers its own policy checks (recursive)

Example:

typescript
OutboundOrder (OUT-001)
├── InternalOrder (PICK-001) - COMPLETED
├── InternalOrder (PACK-001) - COMPLETED
└── ShippingOrder (SHIP-001) - just completed ← TRIGGER

// System automatically:
1. Marks SHIP-001 as COMPLETED
2. Checks siblings: PICK-001 ✅, PACK-001
3. All children complete → auto-complete OUT-001
4. OUT-001 marked as COMPLETED

Cancellation Validation

Parent Cancellation Rules

Define what happens when parent is cancelled:

Example: OutboundOrder Policy

typescript
{
  orderType: 'OutboundOrder',
  parentCancellation: {
    allowWithActiveChildren: false,  // Cannot cancel if children are active
    childAction: 'CASCADE_CANCEL',   // Cancel all children
    requiresApproval: true           // Requires manager approval
  }
}

Validation:

typescript
OutboundOrder (OUT-001) - trying to cancel
├── InternalOrder (PICK-001) - IN_PROGRESS ❌ Active
└── ShippingOrder (SHIP-001) - PENDING ❌ Active

// Validation result:
{
  allowed: false,
  reason: "Cannot cancel: 2 active child orders in progress",
  childAction: 'CASCADE_CANCEL',
  activeChildren: [
    { ref: 'PICK-001', type: 'InternalOrder', status: 'IN_PROGRESS' },
    { ref: 'SHIP-001', type: 'ShippingOrder', status: 'PENDING' }
  ]
}

Child Action Strategies

What should happen to children when parent is cancelled?

CASCADE_CANCEL - Cancel all children

typescript
childAction: 'CASCADE_CANCEL'

// Parent cancelled → automatically cancel all children
OutboundOrder (CANCELLED)
├── InternalOrder (CANCELLED) ← Auto-cancelled
├── InternalOrder (CANCELLED) ← Auto-cancelled
└── ShippingOrder (CANCELLED) ← Auto-cancelled

ORPHAN - Detach children, let them continue

typescript
childAction: 'ORPHAN'

// Parent cancelled → children become independent
OutboundOrder (CANCELLED)
// Children removed from parent, continue independently
InternalOrder (IN_PROGRESS) ← Now orphaned, continues
ShippingOrder (PENDING) ← Now orphaned, continues

COMPLETE_FIRST - Must complete children before cancelling parent

typescript
childAction: 'COMPLETE_FIRST'

// Cannot cancel until all children are complete
OutboundOrder - cannot cancel yet
├── InternalOrder (IN_PROGRESS) ← Must complete first
└── ShippingOrder (PENDING) ← Must complete first

SUSPEND - Suspend children (pause, not cancel)

typescript
childAction: 'SUSPEND'

// Parent cancelled → suspend all children
OutboundOrder (CANCELLED)
├── InternalOrder (SUSPENDED) ← Can resume later
└── ShippingOrder (SUSPENDED) ← Can resume later

Child Cancellation Impact

What happens when a child is cancelled?

Example: InternalOrder Policy

typescript
{
  orderType: 'InternalOrder',
  childCancellation: {
    notifyParent: true,
    triggerParentAction: 'NONE'  // Don't affect parent
  }
}

Or trigger parent failure:

typescript
{
  orderType: 'ShippingOrder',
  childCancellation: {
    notifyParent: true,
    triggerParentAction: 'MARK_FAILED'  // Mark parent as failed
  }
}

Built-in Policies

OutboundOrder

typescript
LSP_LIFECYCLE_POLICIES['OutboundOrder'] = {
  orderType: 'OutboundOrder',
  
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED', 'DELIVERED'],
    blockingChildren: ['InternalOrder', 'ShippingOrder']
  },
  
  childCompletion: {
    autoCompleteParent: true,
    notifyParent: true,
    updateParentStatus: 'READY_TO_SHIP'
  },
  
  parentCancellation: {
    allowWithActiveChildren: false,
    childAction: 'CASCADE_CANCEL',
    requiresApproval: true
  },
  
  childCancellation: {
    notifyParent: true,
    triggerParentAction: 'NONE'
  }
}

Use Case: E-commerce order fulfillment

  • Must complete all tasks (picking, packing, shipping)
  • Children auto-complete parent when all done
  • Cancelling order cancels all tasks
  • Cannot cancel if tasks are in progress

InboundOrder

typescript
LSP_LIFECYCLE_POLICIES['InboundOrder'] = {
  orderType: 'InboundOrder',
  
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED'],
    blockingChildren: ['InternalOrder']
  },
  
  childCompletion: {
    autoCompleteParent: true,
    notifyParent: true
  },
  
  parentCancellation: {
    allowWithActiveChildren: true,  // Can cancel even if receiving in progress
    childAction: 'CASCADE_CANCEL'
  },
  
  childCancellation: {
    notifyParent: true,
    triggerParentAction: 'NONE'
  }
}

Use Case: Receiving operations

  • Must complete all receiving tasks (unload, QC, put-away)
  • More flexible cancellation (vendor no-shows)
  • Children notify parent but don't fail it

InternalOrder

typescript
LSP_LIFECYCLE_POLICIES['InternalOrder'] = {
  orderType: 'InternalOrder',
  
  parentCompletion: {
    requireAllChildrenComplete: false,  // Can complete even with sub-tasks pending
    allowCompletionIfChildrenAre: ['COMPLETED', 'CANCELLED'],
    blockingChildren: []
  },
  
  childCompletion: {
    autoCompleteParent: false,  // Don't auto-complete parent
    notifyParent: true
  },
  
  parentCancellation: {
    allowWithActiveChildren: true,
    childAction: 'CASCADE_CANCEL'
  },
  
  childCancellation: {
    notifyParent: true,
    triggerParentAction: 'NONE'
  }
}

Use Case: Warehouse tasks

  • Flexible completion (some sub-tasks optional)
  • Can cancel even if sub-tasks running
  • More operational flexibility

ShippingOrder

typescript
LSP_LIFECYCLE_POLICIES['ShippingOrder'] = {
  orderType: 'ShippingOrder',
  
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED', 'DELIVERED'],
    blockingChildren: ['InternalOrder']  // Exception handling tasks
  },
  
  childCompletion: {
    autoCompleteParent: false,
    notifyParent: true
  },
  
  parentCancellation: {
    allowWithActiveChildren: false,  // Cannot cancel if driver already dispatched
    childAction: 'SUSPEND',          // Suspend for retry
    requiresApproval: true
  },
  
  childCancellation: {
    notifyParent: true,
    triggerParentAction: 'MARK_FAILED'  // Child failure = delivery failed
  }
}

Use Case: Last-mile delivery

  • Strict completion requirements
  • Cannot easily cancel in-progress delivery
  • Child failure triggers parent failure (delivery exceptions)

PassthroughOrder

typescript
LSP_LIFECYCLE_POLICIES['PassthroughOrder'] = {
  orderType: 'PassthroughOrder',
  
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED', 'SHIPPED'],
    blockingChildren: ['SortingOrder', 'ShippingOrder']
  },
  
  childCompletion: {
    autoCompleteParent: true,  // Fast cross-dock operations
    notifyParent: true
  },
  
  parentCancellation: {
    allowWithActiveChildren: true,  // Time-sensitive, can cancel
    childAction: 'CASCADE_CANCEL'
  },
  
  childCancellation: {
    notifyParent: true,
    triggerParentAction: 'MARK_FAILED'
  }
}

Use Case: Cross-dock hub operations

  • Fast turnaround, auto-complete
  • Time-sensitive, flexible cancellation
  • Child failure impacts parent

Validation Functions

canCompleteOrder

Check if order can be completed:

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

const validation = await canCompleteOrder(
  order,      // BaseOrder object
  orderType,  // 'OutboundOrder', 'InternalOrder', etc.
  db,         // Database connection
  context     // Workspace context
)

// Returns:
{
  allowed: boolean,
  reason?: string,
  blockers?: Array<{
    reference: string,
    type: string,
    currentStatus: string,
    requiredStatuses: string[]
  }>
}

Example Usage:

typescript
// Manual completion route
.patch('/:reference/complete', async (req, rep) => {
  const order = await db.findOne({ reference })
  
  const validation = await canCompleteOrder(order, 'OutboundOrder', db, context)
  
  if (!validation.allowed) {
    return rep.status(400).send({
      error: true,
      message: validation.reason,
      blockers: validation.blockers
    })
  }
  
  // Safe to complete
  await db.updateOne({ reference }, { $set: { status: 'COMPLETED' } })
})

canCancelOrder

Check if order can be cancelled:

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

const validation = await canCancelOrder(
  order,
  orderType,
  db,
  context
)

// Returns:
{
  allowed: boolean,
  reason?: string,
  childAction?: 'CASCADE_CANCEL' | 'ORPHAN' | 'COMPLETE_FIRST' | 'SUSPEND',
  activeChildren?: Array<{
    reference: string,
    type: string,
    status: string
  }>
}

Example Usage:

typescript
// Manual cancellation route
.patch('/:reference/cancel', async (req, rep) => {
  const order = await db.findOne({ reference })
  
  const validation = await canCancelOrder(order, 'OutboundOrder', db, context)
  
  if (!validation.allowed) {
    return rep.status(400).send({
      error: true,
      message: validation.reason,
      activeChildren: validation.activeChildren,
      suggestion: `Required action: ${validation.childAction}`
    })
  }
  
  // Execute cancellation with child action
  await db.updateOne({ reference }, { $set: { status: 'CANCELLED' } })
  await cascadeCancellation(order, validation.childAction, db, context)
})

shouldAutoCompleteParent

Check if child completion should trigger parent auto-completion:

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

const shouldAutoComplete = await shouldAutoCompleteParent(
  childOrder,
  'InternalOrder',
  parentOrder,
  'OutboundOrder',
  db,
  context
)

if (shouldAutoComplete) {
  // All siblings complete → auto-complete parent
  await db.updateOne(
    { reference: parentOrder.reference },
    { $set: { status: 'COMPLETED' } }
  )
}

Parent-Child Synchronization

Update Parent on Child Status Change

Always notify parent when child status changes:

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

// Child completed
await db.updateOne({ reference: childRef }, { $set: { status: 'COMPLETED' } })

// Update parent's child record
await updateParentChildRecord(
  parentRef,
  childRef,
  'COMPLETED',
  db,
  context
)

// Parent's childOrders array updated:
// { ref: childRef, status: 'COMPLETED', ... }

Why this matters:

  • Parent has real-time view of children
  • Validation checks use parent's childOrders array
  • No need to query database for children
  • Faster completion validation

Best Practices

Design Clear Policies

✅ Good Policy:

typescript
{
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED', 'DELIVERED']
  }
}
// Clear: Parent needs all children COMPLETED or DELIVERED

❌ Ambiguous Policy:

typescript
{
  parentCompletion: {
    requireAllChildrenComplete: false,  // Wait, so when can it complete?
    allowCompletionIfChildrenAre: []    // No statuses specified?
  }
}
// Unclear: What are the actual rules?

Use Blocking Flags Wisely

Mandatory children:

typescript
relationship: { type: 'MANDATORY', canBlockParent: true }
// Critical path operations (picking, packing, shipping)

Optional children:

typescript
relationship: { type: 'OPTIONAL', canBlockParent: false }
// Nice-to-have operations (audits, photos, documentation)

Handle Cancellations Carefully

Time-sensitive operations:

typescript
parentCancellation: {
  allowWithActiveChildren: true,  // Can cancel anytime
  childAction: 'CASCADE_CANCEL'   // Clean up children
}
// Example: Cross-dock (time-sensitive)

Committed operations:

typescript
parentCancellation: {
  allowWithActiveChildren: false,  // Cannot cancel once started
  childAction: 'COMPLETE_FIRST',   // Must finish children first
  requiresApproval: true           // Requires manager override
}
// Example: Shipping (driver already dispatched)

Validate Early and Often

❌ Don't skip validation:

typescript
// Dangerous - no validation
await db.updateOne({ ref }, { status: 'COMPLETED' })

✅ Always validate:

typescript
// Safe - validated operation
const validation = await canCompleteOrder(order, type, db, context)
if (!validation.allowed) {
  return { error: true, reason: validation.reason }
}
await db.updateOne({ ref }, { status: 'COMPLETED' })

Use Cases

E-Commerce Order Fulfillment

Policy Requirements:

  • Cannot complete outbound until all tasks done
  • Children auto-complete parent when all done
  • Cancelling order cancels all tasks
  • Cannot cancel if picking started

Implementation:

typescript
LSP_LIFECYCLE_POLICIES['OutboundOrder'] = {
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED', 'DELIVERED']
  },
  childCompletion: {
    autoCompleteParent: true
  },
  parentCancellation: {
    allowWithActiveChildren: false,
    childAction: 'CASCADE_CANCEL'
  }
}

Returns Processing

Policy Requirements:

  • Can complete return even if optional QA pending
  • Can cancel return if defect found
  • Failed QA doesn't fail entire return

Implementation:

typescript
LSP_LIFECYCLE_POLICIES['InboundOrder'] = {
  parentCompletion: {
    requireAllChildrenComplete: false,  // Optional children OK
    allowCompletionIfChildrenAre: ['COMPLETED', 'CANCELLED']
  },
  parentCancellation: {
    allowWithActiveChildren: true,  // Can cancel anytime
    childAction: 'CASCADE_CANCEL'
  }
}

// Mark QA as optional
childOrder.relationship = {
  type: 'OPTIONAL',
  canBlockParent: false
}

Hub Cross-Dock

Policy Requirements:

  • Fast turnaround, auto-complete
  • Can cancel if routing fails
  • All children mandatory (no delays)

Implementation:

typescript
LSP_LIFECYCLE_POLICIES['PassthroughOrder'] = {
  parentCompletion: {
    requireAllChildrenComplete: true,
    allowCompletionIfChildrenAre: ['COMPLETED', 'SHIPPED']
  },
  childCompletion: {
    autoCompleteParent: true  // Fast operations
  },
  parentCancellation: {
    allowWithActiveChildren: true,  // Time-sensitive
    childAction: 'CASCADE_CANCEL'
  }
}

Next Steps


Related Documentation: