Cross-Module Integration for BOM Calculations
Overview
BOM calculations in the Cost Accounting module require data from the Inventory/Production module (Items and BOMs). This document describes the integration event structures designed for cross-module communication.
Integration Event Pattern
Request-Response Pattern
The Cost Accounting module uses a request-response integration event pattern to retrieve data from other modules:
- Cost Accounting publishes a request event (e.g.,
ItemDataRequestedIntegrationEvent) - Inventory Module handles the request and publishes a response event (e.g.,
ItemDataResponseIntegrationEvent) - Cost Accounting receives the response and uses the data for calculations
This pattern ensures:
- Loose coupling: Modules don't directly depend on each other
- Async communication: Non-blocking cross-module calls
- Scalability: Can add more data sources without changing requesters
- Correlation tracking:
RequestIdlinks requests and responses
Event Structure Design
1. ItemDataResponseIntegrationEvent
Purpose: Provides item master data needed for BOM calculations
Key Fields:
Core Identification
ItemId: System identifierItemNumber: User-facing identifierName,Description: Display information
Costing-Relevant Data
-
ItemType: Determines if item is Purchased, Manufactured, Service, etc.- Purchased: Get cost from purchase prices or trade agreements
- Manufactured: Calculate cost from BOM explosion
- Service: Use service cost rates
-
BOMCalcGroupId: Links to calculation group determining cost source rules -
CostGroupId: Categorizes costs (Material, Labor, Overhead, etc.)
Physical Properties
NetWeight,GrossWeight: For freight/logistics cost allocationWeightUnitOfMeasure: Unit for weight measurements
Unit of Measure
DefaultUnitClassId,DefaultUnitClassName: For quantity conversions during calculations
Metadata
IsActive: Exclude inactive items from calculationsIsStockable: Determines if inventory costs are availableCategoryId,CategoryName: For grouping in reports
Error Handling
Found: Boolean indicating if item existsErrorMessage: Reason if not found or error occurredNotFound()factory method: Creates error responses
Usage in BOM Calculations:
// Cost Accounting requests item data
var request = new ItemDataRequestedIntegrationEvent(
requestId: Guid.NewGuid(),
itemId: componentItemId,
includeDetails: true);
await _messageBus.PublishAsync(request);
// Later, when response arrives
public async Task Handle(ItemDataResponseIntegrationEvent response)
{
if (!response.Found)
{
// Log warning in calculation
calculation.AddWarning($"Item {response.ItemId} not found: {response.ErrorMessage}");
return;
}
// Use item data in calculation
var costPriceModel = response.BOMCalcGroupId != null
? await GetCalcGroup(response.BOMCalcGroupId)
: _defaultCalcGroup;
var componentCost = await GetCost(response, costPriceModel);
calculation.AddBOMLine(response.ItemId, quantity, componentCost, response.CostGroupId);
}
2. BOMDataResponseIntegrationEvent
Purpose: Provides BOM structure and routing data for cost roll-up calculations
Key Fields:
BOM Header
BOMId: Unique identifier for the BOMBOMVersion: Version identifier (supports versioning for different configurations)ItemId,ItemNumber: Parent item this BOM producesBOMQuantity: Batch size (all component quantities are per this batch)SiteId: Location-specific BOM (same item may have different BOMs at different plants)
Validity Period
FromDate,ToDate: BOM effective dates- Allows historical calculations
- Supports planned cost changes
BOM Lines (Components)
Each BOMLineDto represents one component in the assembly:
Identification
ItemId,ItemNumber,ItemName: Component identificationLineNum: Sequence/orderingPosition: Physical location in assembly (e.g., "Front Panel", "Rear Axle")
Quantity Calculation
Quantity: Base quantity requiredUnitOfMeasureId: Component's unitConsumptionType:- Constant: Fixed quantity regardless of batch size (e.g., setup materials)
- Variable: Proportional to batch size (standard case)
Scrap Handling
ScrapPercent: Percentage scrap factor (10% = need 10% extra)ScrapConstant: Fixed scrap quantityScrapVariable: Variable scrap per unit- Calculation:
Effective Qty = Quantity * (1 + ScrapPercent/100) + ScrapConstant + (ScrapVariable * BatchQty)
Cost Calculation Support
LineType: Item, Service, Phantom, etc.CostGroupId: Cost category for this componentOperationNum: Which operation consumes this component (for route-based costing)
Multi-Level BOM Support
BOMLevel: Depth in BOM tree (0 = direct component, 1 = sub-component, etc.)IsPhantom: If true, "explode through" to next level without creating production orderSubBOMId: Reference to component's own BOM (for recursive explosion)
Procurement
VendorId: Preferred/required vendor (for subcontracted components)WarehouseId: Specific warehouse/location for picking
Date Validity
FromDate,ToDate: Component effective dates (can differ from BOM header dates)
Route Operations
Each RouteOperationDto represents a manufacturing step:
Sequencing
OperationNum: Sequence numberNextOperationNum: Routing logic (supports parallel operations)Priority: For parallel operations, which runs first
Time Components
SetupTime: One-time setup per batchProcessTime: Time per unitQueueTimeBefore: Wait time before operationTransitTime: Move time after operation
Total Time Calculation:
Total Time = SetupTime + (ProcessTime * Quantity) + QueueTimeBefore + TransitTime
Resource and Cost
ResourceGroupId: Type of resource (e.g., "CNC Machines")ResourceId: Specific machine/workerCostCategoryId: Type of cost (Setup, Run, Machine, Labor)CostGroupId: Cost category for roll-upVendorId: If operation is outsourced
Usage in BOM Calculations:
// Request BOM data
var request = new BOMDataRequestedIntegrationEvent(
requestId: Guid.NewGuid(),
itemId: finishedGoodId,
includeMultiLevel: true, // Explode phantom BOMs
quantity: 100); // Calculation quantity
await _messageBus.PublishAsync(request);
// Process response
public async Task Handle(BOMDataResponseIntegrationEvent response)
{
if (!response.Found)
{
calculation.AddWarning($"BOM not found for item {response.ItemNumber}");
return;
}
// Process BOM lines
foreach (var line in response.Lines)
{
// Calculate effective quantity with scrap
var effectiveQty = CalculateEffectiveQuantity(
line.Quantity,
response.BOMQuantity,
calculationQty: 100,
line.ScrapPercent,
line.ScrapConstant,
line.ScrapVariable,
line.ConsumptionType);
// Get component cost
var componentCost = await GetComponentCost(line.ItemId);
// Add to calculation
calculation.AddBOMLine(
itemId: line.ItemId,
quantity: effectiveQty,
unitCost: componentCost,
costGroupId: line.CostGroupId,
level: line.BOMLevel,
lineNum: line.LineNum);
// Recursive explosion for sub-BOMs
if (line.SubBOMId != null && !line.IsPhantom)
{
await ExplodeSubBOM(line.SubBOMId, effectiveQty, line.BOMLevel + 1);
}
}
// Process route operations
if (response.RouteOperations != null)
{
foreach (var operation in response.RouteOperations)
{
var operationCost = await CalculateOperationCost(
setupTime: operation.SetupTime,
processTime: operation.ProcessTime,
quantity: calculationQty,
resourceId: operation.ResourceId,
costCategoryId: operation.CostCategoryId);
calculation.AddRouteOperation(
operationId: operation.OperationId,
operationNum: operation.OperationNum,
cost: operationCost,
costGroupId: operation.CostGroupId);
}
}
}
Multi-Level BOM Explosion
Single-Level vs Multi-Level
Single-Level Explosion:
- Returns only direct components (BOMLevel = 0)
- Faster, less data transfer
- Used when component costs are already calculated
Multi-Level Explosion:
- Returns entire BOM tree (all levels)
- Necessary for complete cost roll-up
- Used for new/updated items
Phantom BOMs
What They Are: Intermediate assemblies that aren't stocked - explode through to actual components
Example:
Bicycle (Finished Good)
├─ Frame Assembly (PHANTOM - explode through)
│ ├─ Frame Tubing (Level 1)
│ ├─ Welding Supplies (Level 1)
│ └─ Paint (Level 1)
├─ Wheel Assembly (Stocked sub-assembly - Level 0)
└─ Handlebar (Level 0)
In BOM Response:
Lines = [
new BOMLineDto {
ItemNumber = "FRAME-ASSY",
IsPhantom = true,
BOMLevel = 0,
SubBOMId = "BOM-FRAME-001" // Has its own BOM
},
new BOMLineDto {
ItemNumber = "WHEEL-ASSY",
IsPhantom = false, // Stocked item, don't explode
BOMLevel = 0
}
]
Calculation Logic:
if (line.IsPhantom && line.SubBOMId != null)
{
// Don't add phantom as component
// Instead, explode its BOM and add those components
await ExplodePhantomBOM(line.SubBOMId, line.Quantity, level: line.BOMLevel + 1);
}
else
{
// Add as regular component
calculation.AddComponent(line);
}
Error Handling
Not Found Scenarios
Item Not Found:
return ItemDataResponseIntegrationEvent.NotFound(
requestId,
itemId,
errorMessage: "Item does not exist in Inventory module");
BOM Not Found:
return BOMDataResponseIntegrationEvent.NotFound(
requestId,
itemId,
itemNumber,
errorMessage: "No active BOM found for this item");
Validation Warnings
The Inventory module should not validate business rules for costing. Instead:
- Return data as-is
- Let Cost Accounting module apply its validation rules
- Cost Accounting warnings configured in BOMCalcGroup
Example:
// Inventory module - just return data
return new BOMDataResponseIntegrationEvent(
lines: bomLines // Even if some lines have issues
);
// Cost Accounting - validates based on calc group rules
if (calcGroup.WarnNoBOM && response.Lines.Count == 0)
{
calculation.AddWarning("Item has empty BOM");
}
if (calcGroup.WarnNoRoute && response.RouteOperations == null)
{
calculation.AddWarning("Item has no route defined");
}
Calculation Flow Example
Scenario: Calculate cost for "Luxury Perfume 100ml"
1. Request Item Data
└─> ItemDataRequestedIntegrationEvent(ItemId = PERFUME-LUX-100)
2. Receive Item Data
└─> ItemDataResponseIntegrationEvent
├─ ItemType = "Manufactured"
├─ BOMCalcGroupId = "MANF"
└─ CostGroupId = "FINISHED-GOODS"
3. Request BOM Data
└─> BOMDataRequestedIntegrationEvent
├─ ItemId = PERFUME-LUX-100
├─ IncludeMultiLevel = true
└─ Quantity = 100
4. Receive BOM Data
└─> BOMDataResponseIntegrationEvent
├─ BOMQuantity = 1 (recipe for 1 unit)
├─ Lines:
│ ├─ Perfumer's Alcohol (0.085 L) - Material
│ ├─ Fragrance Compound (0.015 L) - Material
│ ├─ Glass Bottle 100ml (1 EA) - Packaging
│ └─ Gift Box Large (1 EA) - Packaging
└─ RouteOperations:
├─ Blending (0.1 hr process time)
├─ Bottling (0.05 hr process time)
└─ Packaging (0.08 hr process time)
5. Calculate Costs
├─ Get component costs (via ItemDataRequested for each)
├─ Calculate operation costs (resource rates * time)
└─ Roll up total cost
6. Result
└─> BOMCalcTable
├─ CostPrice = $45.00
├─ Transactions:
│ ├─ Alcohol: $8.50 (Material)
│ ├─ Fragrance: $15.00 (Material)
│ ├─ Bottle: $5.00 (Packaging)
│ ├─ Box: $4.00 (Packaging)
│ ├─ Blending Labor: $6.00 (Labor)
│ ├─ Bottling Labor: $3.50 (Labor)
│ └─ Packaging Labor: $3.00 (Labor)
└─ SalesPrice = $89.00 (with markup)
Best Practices
For Inventory Module (Event Publishers)
-
Always set Found flag correctly
Found = (item != null) -
Include relevant data for costing
- Item type (purchased vs manufactured)
- Cost group assignments
- Weight for freight calculations
- Calc group overrides
-
Return empty collections, not null
Lines = bomLines ?? new List<BOMLineDto>() -
Denormalize for performance
- Include item names in BOM lines
- Include category names in item responses
- Avoid forcing Cost Accounting to make N+1 requests
For Cost Accounting Module (Event Consumers)
-
Always check Found flag first
if (!response.Found)
{
LogWarning(response.ErrorMessage);
return;
} -
Use correlation IDs
- Track request-response pairs
- Timeout stale requests
- Log orphaned responses
-
Handle partial data gracefully
- Missing route? Warn but continue
- Missing cost group? Use default
- Empty BOM? Check calc group rules
-
Cache responses when appropriate
- Same item requested multiple times in one calculation
- Use short TTL (5-10 minutes)
- Invalidate on relevant events
Future Enhancements
Possible Additions
-
Cost History
- Include historical costs in ItemDataResponse
- Support "cost as of date" calculations
-
Alternative BOMs
- Multiple BOMs per item (different methods)
- Return all, let Cost Accounting choose
-
Batch-Specific BOMs
- BOMs that vary by batch size
- Quantity breaks in components
-
Co-Products/By-Products
- BOMs that produce multiple outputs
- Cost allocation logic
-
BOM Comparison
- Request multiple BOM versions
- Calculate cost differences
-
Formula BOMs
- Process manufacturing formulas
- Concentration calculations