SalesDetails
File: src/IM.Domain/Aggregates/TransactionAggregate/ValueObjects/SalesDetails.cs
Type: Value Object
Module: Inventory - Transactions
Purpose
SalesDetails is a value object that encapsulates type-specific information for Sales transactions. It captures the commercial context of a sale, including customer information, invoice reference, and shipping location. Sales transactions are typically created through integration with the Finance module when sales invoices are posted.
Business Context
Sales transactions record the shipment of inventory to customers, decreasing inventory quantities. They represent the fulfillment side of customer orders and link inventory movements to revenue recognition in the financial system.
Business Value:
- Links inventory to sales revenue (COGS calculation)
- Complete customer order fulfillment trail
- Integration with accounts receivable
- Customer service and returns processing
Financial Impact:
- Decreases inventory asset value
- Triggers Cost of Goods Sold (COGS) expense
- Links to revenue recognition in Finance module
Value Object Properties
CustomerId
Type: Guid | Required: Yes
Purpose: Identifies the customer purchasing the inventory.
Source: Finance module customer master data
ShippingLocationId
Type: Guid | Required: Yes
Purpose: The warehouse location from which inventory is shipped.
Business Rule: Inventory decreased at this location
InvoiceId
Type: Guid? | Required: No
Purpose: Optional link to Finance module invoice record.
Usage: Enables cross-module queries and reconciliation
InvoiceNumber
Type: string | Required: Yes
Purpose: Invoice number for traceability and audit.
Business Rule: Cannot be null or whitespace (required for audit trail)
Description
Type: string? | Required: No
Purpose: Additional context or notes about the sale.
Examples: "Rush order", "Drop ship to customer site", "Partial shipment 1 of 3"
Factory Method
public static SalesDetails Create(
Guid customerId,
Guid shippingLocationId,
Guid? invoiceId,
string invoiceNumber,
string? description = null)
Validation:
- CustomerId cannot be empty
- ShippingLocationId cannot be empty
- InvoiceNumber is required (cannot be null/whitespace)
Usage Example:
var salesDetails = SalesDetails.Create(
customerId: acmeCorpId,
shippingLocationId: warehouseMainId,
invoiceId: financeInvoiceId,
invoiceNumber: "INV-2024-001",
description: "Regular monthly shipment");
Integration with Finance Module
Sales Invoice Processing Flow
Event Data Mapping:
// From SalesInvoiceInventoryPostingRequestedIntegrationEvent
var transaction = InventoryTransaction.CreateSalesTransaction(
userId: Guid.Parse(event.UserId),
customerId: event.CustomerId,
shippingLocationId: event.LocationId,
invoiceId: event.InvoiceId,
invoiceNumber: event.InvoiceNumber,
lineItemData: MapLineItems(event.LineItems),
reason: $"Sales invoice {event.InvoiceNumber}",
externalReference: event.InvoiceNumber,
description: event.Description);
Usage Examples
Example 1: Regular Customer Order
var transaction = InventoryTransaction.CreateSalesTransaction(
userId: salesRepId,
customerId: acmeCorpId,
shippingLocationId: centralWarehouseId,
invoiceId: financeInvoiceGuid,
invoiceNumber: "INV-2024-0156",
lineItemData: new[]
{
new InventoryTransaction.LineItemData(widgetAId, 100, eachUnitId),
new InventoryTransaction.LineItemData(widgetBId, 50, eachUnitId)
},
reason: "Customer order #SO-2024-0892",
externalReference: "SO-2024-0892",
description: "Regular monthly shipment");
Example 2: Rush Order
var transaction = InventoryTransaction.CreateSalesTransaction(
userId: salesManagerId,
customerId: urgentCustomerId,
shippingLocationId: closestWarehouseId,
invoiceId: rushInvoiceId,
invoiceNumber: "INV-2024-RUSH-045",
lineItemData: new[]
{
new InventoryTransaction.LineItemData(urgentPartId, 25, eachUnitId)
},
reason: "Emergency rush order - production line down",
externalReference: "RUSH-ORDER-045",
description: "URGENT - Express shipping to customer site");
Value Object Equality
protected override IEnumerable<object> GetEqualityComponents()
{
yield return CustomerId;
yield return ShippingLocationId;
yield return InvoiceId ?? Guid.Empty;
yield return InvoiceNumber;
yield return Description ?? string.Empty;
}
Database Persistence
builder.OwnsOne(t => t.SalesDetails, sd =>
{
sd.Property(d => d.CustomerId)
.HasColumnName("sales_customer_id");
sd.Property(d => d.ShippingLocationId)
.HasColumnName("sales_shipping_location_id");
sd.Property(d => d.InvoiceId)
.HasColumnName("sales_invoice_id");
sd.Property(d => d.InvoiceNumber)
.HasColumnName("sales_invoice_number")
.HasMaxLength(100);
sd.Property(d => d.Description)
.HasColumnName("sales_description")
.HasMaxLength(500);
});
Testing Strategies
[TestFixture]
public class SalesDetailsTests
{
[Test]
public void Create_ValidInputs_CreatesSalesDetails()
{
// Arrange
var customerId = Guid.NewGuid();
var locationId = Guid.NewGuid();
var invoiceId = Guid.NewGuid();
// Act
var details = SalesDetails.Create(customerId, locationId, invoiceId, "INV-001");
// Assert
Assert.That(details.CustomerId, Is.EqualTo(customerId));
Assert.That(details.InvoiceNumber, Is.EqualTo("INV-001"));
}
[Test]
public void Create_EmptyCustomerId_ThrowsException()
{
// Act & Assert
var ex = Assert.Throws<DomainException>(() =>
SalesDetails.Create(Guid.Empty, Guid.NewGuid(), null, "INV-001"));
Assert.That(ex.Message, Does.Contain("CustomerId"));
}
[Test]
public void Create_EmptyInvoiceNumber_ThrowsException()
{
// Act & Assert
var ex = Assert.Throws<DomainException>(() =>
SalesDetails.Create(Guid.NewGuid(), Guid.NewGuid(), null, ""));
Assert.That(ex.Message, Does.Contain("InvoiceNumber is required"));
}
}
Related Documentation
Last Updated: 2025-10-23 | Version: 1.0 | Status: Production Ready