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:
// ❌ 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 warehouseWith Validation:
// ✅ 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:
{
// 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
{
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:
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:
// Root order with no children
OutboundOrder (OUT-001)
// No childOrders array or empty array
// ✅ Can always complete - no children to validateWhy? 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:
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 parentValidation Logic:
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:
// 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:
{
orderType: 'InternalOrder',
childCompletion: {
autoCompleteParent: true,
notifyParent: true
}
}How it works:
- Child order completes
- System checks if all sibling children are complete
- If yes, automatically completes parent
- Parent's completion triggers its own policy checks (recursive)
Example:
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 COMPLETEDCancellation Validation
Parent Cancellation Rules
Define what happens when parent is cancelled:
Example: OutboundOrder Policy
{
orderType: 'OutboundOrder',
parentCancellation: {
allowWithActiveChildren: false, // Cannot cancel if children are active
childAction: 'CASCADE_CANCEL', // Cancel all children
requiresApproval: true // Requires manager approval
}
}Validation:
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
childAction: 'CASCADE_CANCEL'
// Parent cancelled → automatically cancel all children
OutboundOrder (CANCELLED)
├── InternalOrder (CANCELLED) ← Auto-cancelled
├── InternalOrder (CANCELLED) ← Auto-cancelled
└── ShippingOrder (CANCELLED) ← Auto-cancelledORPHAN - Detach children, let them continue
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, continuesCOMPLETE_FIRST - Must complete children before cancelling parent
childAction: 'COMPLETE_FIRST'
// Cannot cancel until all children are complete
OutboundOrder - cannot cancel yet
├── InternalOrder (IN_PROGRESS) ← Must complete first
└── ShippingOrder (PENDING) ← Must complete firstSUSPEND - Suspend children (pause, not cancel)
childAction: 'SUSPEND'
// Parent cancelled → suspend all children
OutboundOrder (CANCELLED)
├── InternalOrder (SUSPENDED) ← Can resume later
└── ShippingOrder (SUSPENDED) ← Can resume laterChild Cancellation Impact
What happens when a child is cancelled?
Example: InternalOrder Policy
{
orderType: 'InternalOrder',
childCancellation: {
notifyParent: true,
triggerParentAction: 'NONE' // Don't affect parent
}
}Or trigger parent failure:
{
orderType: 'ShippingOrder',
childCancellation: {
notifyParent: true,
triggerParentAction: 'MARK_FAILED' // Mark parent as failed
}
}Built-in Policies
OutboundOrder
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
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
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
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
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:
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:
// 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:
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:
// 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:
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:
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:
{
parentCompletion: {
requireAllChildrenComplete: true,
allowCompletionIfChildrenAre: ['COMPLETED', 'DELIVERED']
}
}
// Clear: Parent needs all children COMPLETED or DELIVERED❌ Ambiguous Policy:
{
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:
relationship: { type: 'MANDATORY', canBlockParent: true }
// Critical path operations (picking, packing, shipping)Optional children:
relationship: { type: 'OPTIONAL', canBlockParent: false }
// Nice-to-have operations (audits, photos, documentation)Handle Cancellations Carefully
Time-sensitive operations:
parentCancellation: {
allowWithActiveChildren: true, // Can cancel anytime
childAction: 'CASCADE_CANCEL' // Clean up children
}
// Example: Cross-dock (time-sensitive)Committed operations:
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:
// Dangerous - no validation
await db.updateOne({ ref }, { status: 'COMPLETED' })✅ Always validate:
// 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:
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:
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:
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:

