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

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"));
}
}


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