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

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

  1. Purchase Order Created (Procurement system)
  2. Goods Receipt (Inventory creates Purchase transaction)
  3. 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"));
}
}


Last Updated: 2025-10-23 | Version: 1.0 | Status: Production Ready