انتقل إلى المحتوى الرئيسي

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:

  1. Cost Accounting publishes a request event (e.g., ItemDataRequestedIntegrationEvent)
  2. Inventory Module handles the request and publishes a response event (e.g., ItemDataResponseIntegrationEvent)
  3. 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: RequestId links requests and responses

Event Structure Design

1. ItemDataResponseIntegrationEvent

Purpose: Provides item master data needed for BOM calculations

Key Fields:

Core Identification

  • ItemId: System identifier
  • ItemNumber: User-facing identifier
  • Name, 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 allocation
  • WeightUnitOfMeasure: Unit for weight measurements

Unit of Measure

  • DefaultUnitClassId, DefaultUnitClassName: For quantity conversions during calculations

Metadata

  • IsActive: Exclude inactive items from calculations
  • IsStockable: Determines if inventory costs are available
  • CategoryId, CategoryName: For grouping in reports

Error Handling

  • Found: Boolean indicating if item exists
  • ErrorMessage: Reason if not found or error occurred
  • NotFound() 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 BOM
  • BOMVersion: Version identifier (supports versioning for different configurations)
  • ItemId, ItemNumber: Parent item this BOM produces
  • BOMQuantity: 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 identification
  • LineNum: Sequence/ordering
  • Position: Physical location in assembly (e.g., "Front Panel", "Rear Axle")
Quantity Calculation
  • Quantity: Base quantity required
  • UnitOfMeasureId: Component's unit
  • ConsumptionType:
    • 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 quantity
  • ScrapVariable: 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 component
  • OperationNum: 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 order
  • SubBOMId: 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 number
  • NextOperationNum: Routing logic (supports parallel operations)
  • Priority: For parallel operations, which runs first
Time Components
  • SetupTime: One-time setup per batch
  • ProcessTime: Time per unit
  • QueueTimeBefore: Wait time before operation
  • TransitTime: 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/worker
  • CostCategoryId: Type of cost (Setup, Run, Machine, Labor)
  • CostGroupId: Cost category for roll-up
  • VendorId: 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)

  1. Always set Found flag correctly

    Found = (item != null)
  2. Include relevant data for costing

    • Item type (purchased vs manufactured)
    • Cost group assignments
    • Weight for freight calculations
    • Calc group overrides
  3. Return empty collections, not null

    Lines = bomLines ?? new List<BOMLineDto>()
  4. 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)

  1. Always check Found flag first

    if (!response.Found)
    {
    LogWarning(response.ErrorMessage);
    return;
    }
  2. Use correlation IDs

    • Track request-response pairs
    • Timeout stale requests
    • Log orphaned responses
  3. Handle partial data gracefully

    • Missing route? Warn but continue
    • Missing cost group? Use default
    • Empty BOM? Check calc group rules
  4. 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

  1. Cost History

    • Include historical costs in ItemDataResponse
    • Support "cost as of date" calculations
  2. Alternative BOMs

    • Multiple BOMs per item (different methods)
    • Return all, let Cost Accounting choose
  3. Batch-Specific BOMs

    • BOMs that vary by batch size
    • Quantity breaks in components
  4. Co-Products/By-Products

    • BOMs that produce multiple outputs
    • Cost allocation logic
  5. BOM Comparison

    • Request multiple BOM versions
    • Calculate cost differences
  6. Formula BOMs

    • Process manufacturing formulas
    • Concentration calculations