Skip to main content

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

  1. Event Publication Rate: Events/minute published to message bus
  2. Event Processing Latency: Time from publication to GL posting
  3. Failed Events: Count of events in retry queue or DLQ
  4. 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));
}

Last Updated: 2025-10-24 | Status: Active | Version: 1.0