PurchaseDetails
File: src/IM.Domain/Aggregates/TransactionAggregate/ValueObjects/PurchaseDetails.cs
Type: Value Object
Module: Inventory - Transactions
Purpose
PurchaseDetails is a value object that encapsulates type-specific information for Purchase transactions. It captures the commercial context of a purchase, including vendor information, invoice reference, and receiving location. Purchase transactions are typically created through integration with the Finance module when purchase invoices are posted.
Business Context
Purchase transactions record the receipt of inventory from vendors, increasing inventory quantities. They represent the receiving side of purchase orders and link inventory additions to accounts payable in the financial system.
Business Value:
- Links inventory to vendor invoices (AP)
- Complete purchase order receiving trail
- Integration with accounts payable
- Vendor performance tracking
- Purchase cost tracking for inventory valuation
Financial Impact:
- Increases inventory asset value
- Creates accounts payable liability
- Establishes inventory cost basis
- Enables FIFO/LIFO/Average cost calculations
Value Object Properties
VendorId
Type: Guid | Required: Yes
Purpose: Identifies the vendor supplying the inventory.
Source: Finance module vendor master data
ReceivingLocationId
Type: Guid | Required: Yes
Purpose: The warehouse location where inventory is received.
Business Rule: Inventory increased at this location
InvoiceId
Type: Guid? | Required: No
Purpose: Optional link to Finance module invoice record.
Usage: Enables cross-module queries and three-way matching (PO-Receipt-Invoice)
InvoiceNumber
Type: string | Required: Yes
Purpose: Vendor invoice number for traceability and audit.
Business Rule: Cannot be null or whitespace (required for AP matching)
Description
Type: string? | Required: No
Purpose: Additional context or notes about the purchase.
Examples: "Emergency order", "Quarterly bulk purchase", "Drop ship from manufacturer"
Factory Method
public static PurchaseDetails Create(
Guid vendorId,
Guid receivingLocationId,
Guid? invoiceId,
string invoiceNumber,
string? description)
Validation:
- VendorId cannot be empty
- ReceivingLocationId cannot be empty
- InvoiceNumber is required (cannot be null/whitespace)
Usage Example:
var purchaseDetails = PurchaseDetails.Create(
vendorId: acmeSupplyId,
receivingLocationId: receivingDockId,
invoiceId: financeInvoiceId,
invoiceNumber: "VENDOR-INV-5678",
description: "Monthly bulk order");
Integration with Finance Module
Purchase Invoice Processing Flow
Event Data Mapping:
// From PurchaseInvoiceInventoryPostingRequestedIntegrationEvent
var transaction = InventoryTransaction.CreatePurchaseTransaction(
userId: Guid.Parse(event.UserId),
vendorId: event.VendorId,
receivingLocationId: event.LocationId,
invoiceId: event.InvoiceId,
invoiceNumber: event.InvoiceNumber,
lineItemData: MapLineItems(event.LineItems),
reason: $"Purchase invoice {event.InvoiceNumber}",
externalReference: event.PurchaseOrderNumber,
description: event.Description);
Three-Way Matching
Purchase Order → Receipt → Invoice
- Purchase Order Created (Procurement system)
- Goods Receipt (Inventory creates Purchase transaction)
- Vendor Invoice (Finance matches to receipt)
PurchaseDetails Role:
- Links inventory receipt to vendor invoice
- Enables matching quantity received vs invoiced
- Supports variance analysis
- Provides audit trail
Usage Examples
Example 1: Regular Vendor Shipment
var transaction = InventoryTransaction.CreatePurchaseTransaction(
userId: receivingClerkId,
vendorId: reliableVendorId,
receivingLocationId: mainWarehouseReceivingId,
invoiceId: financeInvoiceGuid,
invoiceNumber: "VENDOR-45678",
lineItemData: new[]
{
new InventoryTransaction.LineItemData(rawMaterialAId, 500, kgUnitId),
new InventoryTransaction.LineItemData(rawMaterialBId, 250, kgUnitId)
},
reason: "Purchase order #PO-2024-0234 receipt",
externalReference: "PO-2024-0234",
description: "Regular monthly raw material shipment");
Example 2: Emergency Order
var transaction = InventoryTransaction.CreatePurchaseTransaction(
userId: purchasingManagerId,
vendorId: expediteVendorId,
receivingLocationId: productionAreaId,
invoiceId: rushInvoiceId,
invoiceNumber: "RUSH-INV-001",
lineItemData: new[]
{
new InventoryTransaction.LineItemData(criticalPartId, 100, eachUnitId)
},
reason: "Emergency purchase - production line down",
externalReference: "EMERGENCY-PO-045",
description: "URGENT - Expedited delivery, direct to production floor");
Example 3: Consignment Receipt
var transaction = InventoryTransaction.CreatePurchaseTransaction(
userId: warehouseManagerId,
vendorId: consignmentVendorId,
receivingLocationId: consignmentStorageId,
invoiceId: null, // No invoice yet (consignment)
invoiceNumber: "CONSIGNMENT-2024-Q1",
lineItemData: new[]
{
new InventoryTransaction.LineItemData(consignmentItemId, 1000, eachUnitId)
},
reason: "Consignment stock receipt - invoiced on usage",
externalReference: "CONSIGN-AGREEMENT-2024",
description: "Consignment inventory - pay on use");
Value Object Equality
protected override IEnumerable<object> GetEqualityComponents()
{
yield return VendorId;
yield return ReceivingLocationId;
yield return InvoiceId ?? Guid.Empty;
yield return InvoiceNumber;
yield return Description ?? string.Empty;
}
Database Persistence
builder.OwnsOne(t => t.PurchaseDetails, pd =>
{
pd.Property(d => d.VendorId)
.HasColumnName("purchase_vendor_id");
pd.Property(d => d.ReceivingLocationId)
.HasColumnName("purchase_receiving_location_id");
pd.Property(d => d.InvoiceId)
.HasColumnName("purchase_invoice_id");
pd.Property(d => d.InvoiceNumber)
.HasColumnName("purchase_invoice_number")
.HasMaxLength(100);
pd.Property(d => d.Description)
.HasColumnName("purchase_description")
.HasMaxLength(500);
});
Testing Strategies
[TestFixture]
public class PurchaseDetailsTests
{
[Test]
public void Create_ValidInputs_CreatesPurchaseDetails()
{
// Arrange
var vendorId = Guid.NewGuid();
var locationId = Guid.NewGuid();
var invoiceId = Guid.NewGuid();
// Act
var details = PurchaseDetails.Create(
vendorId, locationId, invoiceId, "VENDOR-INV-001", "Test");
// Assert
Assert.That(details.VendorId, Is.EqualTo(vendorId));
Assert.That(details.InvoiceNumber, Is.EqualTo("VENDOR-INV-001"));
}
[Test]
public void Create_EmptyVendorId_ThrowsException()
{
// Act & Assert
var ex = Assert.Throws<DomainException>(() =>
PurchaseDetails.Create(Guid.Empty, Guid.NewGuid(), null, "INV-001", null));
Assert.That(ex.Message, Does.Contain("VendorId"));
}
[Test]
public void Create_EmptyInvoiceNumber_ThrowsException()
{
// Act & Assert
var ex = Assert.Throws<DomainException>(() =>
PurchaseDetails.Create(Guid.NewGuid(), Guid.NewGuid(), null, "", null));
Assert.That(ex.Message, Does.Contain("InvoiceNumber is required"));
}
}
Related Documentation
Last Updated: 2025-10-23 | Version: 1.0 | Status: Production Ready