Finance Module Integration
Overview
The Inventory Management Module integrates with the Finance Module through integration events to ensure that inventory transactions are properly reflected in the general ledger. This integration maintains financial accuracy and supports compliance with accounting standards.
Business Context
Accounting Principle: Inventory movements affect financial accounts:
- Purchase Receipts: Increase Inventory asset account, increase Accounts Payable
- Sales Shipments: Decrease Inventory asset account, increase Cost of Goods Sold
- Assembly Operations: Transfer from Raw Materials to Finished Goods
- Adjustments: Adjust Inventory account, offset to Variance/Shrinkage account
The Finance Module needs to know about these inventory movements to create proper journal entries.
Integration Architecture
Event-Driven Integration
Integration Events
1. Purchase Receipt Event
Trigger: Purchase transaction created in inventory
Event: PurchaseReceiptCompletedEvent
Payload:
{
"eventId": "event-20250124-001",
"eventType": "PurchaseReceiptCompleted",
"timestamp": "2025-01-24T10:00:00Z",
"transactionId": "trans-20250124-001",
"transactionDate": "2025-01-24T10:00:00Z",
"vendorId": "vendor-001-id",
"purchaseOrderId": "PO-2025-001",
"invoiceNumber": "INV-2025-001",
"locationId": "warehouse-main-id",
"items": [
{
"itemId": "steel-plate-id",
"itemNumber": "RM-STEEL-001",
"quantity": 100.0,
"unitOfMeasureId": "kg-unit-id",
"unitCost": 25.50,
"totalCost": 2550.00
}
],
"totalAmount": 2550.00,
"currencyCode": "USD"
}
Finance Module Action:
Journal Entry:
Debit: Inventory Asset (Raw Materials) $2,550.00
Credit: Accounts Payable $2,550.00
Description: Purchase receipt - PO-2025-001, Invoice INV-2025-001
2. Sales Shipment Event
Trigger: Sales transaction created in inventory
Event: SalesShipmentCompletedEvent
Payload:
{
"eventId": "event-20250124-002",
"eventType": "SalesShipmentCompleted",
"timestamp": "2025-01-24T14:00:00Z",
"transactionId": "trans-20250124-002",
"transactionDate": "2025-01-24T14:00:00Z",
"customerId": "customer-001-id",
"salesOrderId": "SO-2025-001",
"invoiceNumber": "INV-OUT-2025-001",
"locationId": "warehouse-main-id",
"items": [
{
"itemId": "widget-001-id",
"itemNumber": "WIDGET-001",
"quantity": 50.0,
"unitOfMeasureId": "each-unit-id",
"unitCost": 75.00,
"totalCost": 3750.00
}
],
"totalCost": 3750.00,
"totalRevenue": 5000.00,
"currencyCode": "USD"
}
Finance Module Action:
Journal Entry #1 (Revenue Recognition):
Debit: Accounts Receivable $5,000.00
Credit: Sales Revenue $5,000.00
Journal Entry #2 (Cost of Goods Sold):
Debit: Cost of Goods Sold $3,750.00
Credit: Inventory Asset (Finished Goods) $3,750.00
Description: Sales shipment - SO-2025-001, Invoice INV-OUT-2025-001
3. Assembly Completed Event
Trigger: Assembly transaction created in inventory
Event: AssemblyCompletedEvent
Payload:
{
"eventId": "event-20250124-003",
"eventType": "AssemblyCompleted",
"timestamp": "2025-01-24T16:00:00Z",
"transactionId": "trans-20250124-003",
"transactionDate": "2025-01-24T16:00:00Z",
"bomId": "bom-widget-001",
"producedItemId": "widget-001-id",
"producedItemNumber": "WIDGET-001",
"producedQuantity": 100.0,
"locationId": "production-floor-id",
"componentsCost": 6500.00,
"finishedGoodsValue": 7500.00,
"currencyCode": "USD",
"components": [
{
"itemId": "steel-frame-id",
"quantity": 100.0,
"cost": 2500.00
},
{
"itemId": "motor-id",
"quantity": 100.0,
"cost": 3000.00
},
{
"itemId": "paint-id",
"quantity": 50.0,
"cost": 1000.00
}
]
}
Finance Module Action:
Journal Entry (WIP to Finished Goods):
Debit: Inventory Asset (Finished Goods) $7,500.00
Credit: Inventory Asset (Raw Materials) $6,500.00
Credit: Manufacturing Overhead Applied $1,000.00
Description: Assembly - 100 units of WIDGET-001, BOM: bom-widget-001
4. Inventory Adjustment Event
Trigger: Stock adjustment transaction created
Event: InventoryAdjustmentCompletedEvent
Payload:
{
"eventId": "event-20250124-004",
"eventType": "InventoryAdjustmentCompleted",
"timestamp": "2025-01-24T18:00:00Z",
"transactionId": "trans-20250124-004",
"transactionDate": "2025-01-24T18:00:00Z",
"adjustmentType": "Negative",
"reason": "Cycle count correction - shrinkage detected",
"locationId": "warehouse-main-id",
"items": [
{
"itemId": "bolt-m10-id",
"itemNumber": "HW-BOLT-M10",
"quantityAdjustment": -50.0,
"unitOfMeasureId": "each-unit-id",
"unitCost": 0.25,
"totalValue": -12.50
}
],
"totalValue": -12.50,
"currencyCode": "USD"
}
Finance Module Action:
Journal Entry (Inventory Shrinkage):
Debit: Inventory Shrinkage Expense $12.50
Credit: Inventory Asset $12.50
Description: Inventory adjustment - Cycle count correction
Event Flow Details
Complete Integration Flow
Data Mapping
Inventory to GL Accounts
Purchase Receipts:
Inventory Transaction → GL Accounts
Purchase (Debit) → Inventory Asset - Raw Materials
→ Inventory Asset - Purchased Parts
→ Inventory Asset - Supplies
Purchase (Credit) → Accounts Payable
Sales Shipments:
Inventory Transaction → GL Accounts
Sales (Debit) → Accounts Receivable
Sales (Credit) → Sales Revenue
COGS (Debit) → Cost of Goods Sold
COGS (Credit) → Inventory Asset - Finished Goods
Assembly:
Inventory Transaction → GL Accounts
Assembly (Debit) → Inventory Asset - Finished Goods
→ Work in Process
Assembly (Credit) → Inventory Asset - Raw Materials
→ Manufacturing Overhead
Adjustments:
Inventory Transaction → GL Accounts
Positive Adjustment → Inventory Asset (Debit)
→ Inventory Gain (Credit)
Negative Adjustment → Inventory Shrinkage (Debit)
→ Inventory Asset (Credit)
Account Determination
Item-Based Account Mapping
{
"item": {
"id": "steel-plate-id",
"itemNumber": "RM-STEEL-001",
"categoryId": "raw-materials-category",
"accountMapping": {
"inventoryAccount": "1300 - Inventory Raw Materials",
"cogsAccount": "5100 - Cost of Goods Sold - Materials",
"varianceAccount": "5300 - Inventory Variance"
}
}
}
Category-Based Account Mapping
{
"category": {
"id": "raw-materials-category",
"code": "RAW-MAT",
"name": "Raw Materials",
"accountMapping": {
"inventoryAccount": "1300",
"cogsAccount": "5100",
"varianceAccount": "5300"
}
}
}
Error Handling
Event Publication Failures
Problem: RabbitMQ unavailable when publishing event
Solution:
1. Store event in Outbox table
2. Background service retries publication
3. Ensures eventual consistency
4. No data loss
Event Consumption Failures
Problem: Finance module cannot process event (GL period closed, etc.)
Solution:
1. Event goes to retry queue
2. Exponential backoff retry policy
3. After max retries → Dead Letter Queue (DLQ)
4. Manual intervention for DLQ messages
Duplicate Event Handling
Problem: Same event delivered multiple times
Solution:
1. Finance module tracks processed event IDs
2. Idempotency check before processing
3. If already processed → Acknowledge & skip
4. Ensures GL entries not duplicated
Testing Integration
Integration Test Scenario
[Test]
public async Task PurchaseReceipt_Should_Create_GL_Entry()
{
// Arrange
var purchase = CreatePurchaseTransaction(
itemId: "steel-plate-id",
quantity: 100,
unitCost: 25.50m);
// Act
await inventoryService.ProcessPurchaseAsync(purchase);
// Assert - Verify event published
var publishedEvent = await GetPublishedEvent<PurchaseReceiptCompletedEvent>();
Assert.NotNull(publishedEvent);
Assert.Equal(2550.00m, publishedEvent.TotalAmount);
// Assert - Verify GL entry created (via Finance module)
await WaitForEventProcessing();
var glEntry = await financeService.GetJournalByReference(purchase.Id);
Assert.NotNull(glEntry);
Assert.Equal(2550.00m, glEntry.TotalDebit);
Assert.Equal(2550.00m, glEntry.TotalCredit);
}
Configuration
Inventory Module Configuration
{
"Integration": {
"Finance": {
"Enabled": true,
"PublishEvents": true,
"RabbitMQ": {
"Host": "rabbitmq.company.local",
"Exchange": "inventory.events",
"RoutingKeys": {
"Purchase": "inventory.purchase.completed",
"Sales": "inventory.sales.completed",
"Assembly": "inventory.assembly.completed",
"Adjustment": "inventory.adjustment.completed"
}
}
}
}
}
Finance Module Configuration
{
"Integration": {
"Inventory": {
"Enabled": true,
"ConsumeEvents": true,
"RabbitMQ": {
"Host": "rabbitmq.company.local",
"Queue": "finance.inventory.events",
"Exchange": "inventory.events",
"BindingKeys": [
"inventory.purchase.completed",
"inventory.sales.completed",
"inventory.assembly.completed",
"inventory.adjustment.completed"
]
},
"AutoPostJournals": true,
"AccountMappings": {
"InventoryAsset": "1300",
"COGS": "5100",
"Revenue": "4000",
"Shrinkage": "5300"
}
}
}
}
Monitoring and Observability
Key Metrics
- Event Publication Rate: Events/minute published to message bus
- Event Processing Latency: Time from publication to GL posting
- Failed Events: Count of events in retry queue or DLQ
- GL Posting Success Rate: Percentage of events successfully posted
Monitoring Dashboard
┌─────────────────────────────────────────────────┐
│ Inventory-Finance Integration Dashboard │
├─────────────────────────────────────────────────┤
│ Events Published Today: 1,234 │
│ Events Processed: 1,230 │
│ Events In Retry: 3 │
│ Events In DLQ: 1 │
│ │
│ Average Processing Latency: 2.3s │
│ Success Rate: 99.67% │
│ │
│ Recent Failures: │
│ • Event-20250124-789: GL period closed │
│ Status: Retry scheduled │
│ │
│ [View Retry Queue] [View DLQ] [Reprocess] │
└─────────────────────────────────────────────────┘
Best Practices
1. Idempotent Event Consumers
public class PurchaseReceiptConsumer : IConsumer<PurchaseReceiptCompletedEvent>
{
public async Task Consume(ConsumeContext<PurchaseReceiptCompletedEvent> context)
{
var eventId = context.Message.EventId;
// Check if already processed
if (await _processedEvents.ExistsAsync(eventId))
{
_logger.LogInformation("Event {EventId} already processed", eventId);
return; // Idempotency - skip duplicate
}
// Process event
await ProcessPurchaseReceipt(context.Message);
// Mark as processed
await _processedEvents.AddAsync(eventId);
}
}
2. Correlation IDs
// Include correlation ID in events
var event = new PurchaseReceiptCompletedEvent
{
CorrelationId = purchaseTransaction.Id,
// ... other properties
};
// Finance module can correlate back to inventory transaction
3. Compensating Transactions
// If GL posting fails after inventory updated
public async Task CompensateFailedPosting(InventoryTransaction transaction)
{
// Create reversing inventory transaction
var reversal = transaction.CreateReversal("GL posting failed - compensation");
await _transactionRepo.AddAsync(reversal);
await _unitOfWork.CommitAsync();
// Publish compensation event
await _eventBus.PublishAsync(new TransactionReversedEvent(reversal));
}
Related Documentation
- Transaction Aggregate - Transaction structure
- Transactions API - Transaction endpoints
- Event-Driven Architecture - Event patterns
Last Updated: 2025-10-24 | Status: Active | Version: 1.0