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

AdjustmentDetails

File: src/IM.Domain/Aggregates/TransactionAggregate/ValueObjects/AdjustmentDetails.cs Type: Value Object Module: Inventory - Transactions

Purpose

AdjustmentDetails is a value object that encapsulates type-specific information for Adjustment transactions. It represents corrections to inventory quantities at a single location due to cycle counts, damage, loss, or found inventory. Unlike movements (which shift inventory between locations) or purchases/sales (which affect total inventory), adjustments correct discrepancies between recorded and actual inventory.

Business Context

Inventory Adjustments in Operations

Adjustments are critical for maintaining inventory accuracy. They represent the reality check between what the system thinks you have versus what you actually have.

Common Business Scenarios:

  • Cycle Counting: Regular physical counts revealing discrepancies
  • Damage/Shrinkage: Recording damaged, stolen, or expired items
  • Found Inventory: Discovering previously unrecorded items
  • System Corrections: Fixing data entry errors or system bugs
  • Quality Rejections: Removing defective items from available inventory

Business Value:

  • Maintains inventory accuracy (critical for financial reporting)
  • Provides audit trail for discrepancies
  • Enables root cause analysis of inventory issues
  • Supports compliance with accounting standards

Financial Impact:

  • Positive adjustments increase asset value on balance sheet
  • Negative adjustments may flow to cost of goods sold or shrinkage expense
  • Large adjustments may require investigation and approval

Value Object Properties

LocationId

Type: Guid Required: Yes Purpose: The single location where the inventory adjustment occurred.

Business Rules:

  • Cannot be empty (Guid.Empty)
  • Must reference an existing location (validated at application layer)
  • Adjustments affect only this location (single-location operation)

Why Single Location: Adjustments correct inventory at a specific location. If inventory needs to move between locations, use Movement transactions instead.


IsPositiveAdjustment

Type: bool Required: Yes Purpose: Indicates whether the adjustment increased (true) or decreased (false) inventory.

Business Logic:

  • True (Positive): Inventory increased (found inventory, correction upward)
  • False (Negative): Inventory decreased (shrinkage, damage, correction downward)

Calculation (in InventoryTransaction factory method):

decimal adjustmentQuantity = newQuantity - currentQuantity;
bool isPositiveAdjustment = adjustmentQuantity > 0;

// Examples:
// Current: 100, New: 150 → Adjustment: +50 → IsPositive: true
// Current: 100, New: 95 → Adjustment: -5 → IsPositive: false

Why Separate Flag:

  • Clear intent for reporting and audit
  • Simplifies inventory update logic
  • Enables easy filtering of increases vs decreases
  • Line item quantity is always positive (absolute value)

Factory Methods

Create

Purpose: Creates an AdjustmentDetails value object with validation.

public static AdjustmentDetails Create(
Guid locationId,
bool isPositiveAdjustment)

Validation Rules:

  1. Location ID cannot be empty

Usage Example:

// Positive adjustment (found inventory)
var positiveDetails = AdjustmentDetails.Create(
locationId: warehouseAId,
isPositiveAdjustment: true);

// Negative adjustment (shrinkage)
var negativeDetails = AdjustmentDetails.Create(
locationId: warehouseAId,
isPositiveAdjustment: false);

Throws: DomainException if validation fails


Integration with InventoryTransaction

Adjustment Transaction Creation

public static InventoryTransaction CreateAdjustment(
Guid userId,
Guid locationId,
decimal currentQuantity,
decimal newQuantity,
Guid itemId,
Guid unitOfMeasureId,
string reason,
string? externalReference = null,
string? notes = null)
{
// Domain calculates adjustment direction
decimal adjustmentQuantity = newQuantity - currentQuantity;
bool isPositiveAdjustment = adjustmentQuantity > 0;

// Validate no zero adjustment
if (adjustmentQuantity == 0)
throw new DomainException("New quantity is the same as current quantity. No adjustment needed.");

// Create transaction
var transaction = new InventoryTransaction(
TransactionType.Adjustment,
userId,
DateTime.UtcNow,
reason.Trim(),
externalReference?.Trim());

// Create AdjustmentDetails
transaction.AdjustmentDetails = AdjustmentDetails.Create(
locationId,
isPositiveAdjustment);

// Create line item with ABSOLUTE quantity
var lineItems = new List<TransactionLineItem>
{
TransactionLineItem.Create(
transaction.Id,
itemId,
Math.Abs(adjustmentQuantity), // Always positive!
unitOfMeasureId,
notes)
};

transaction.AddLineItems(lineItems);
return transaction;
}

Key Domain Logic:

  • Factory method calculates adjustment quantity and direction
  • AdjustmentDetails stores only the direction flag
  • Line item quantity is absolute value
  • Direction + Quantity = Complete adjustment information

Domain Methods

GetAdjustmentDescription

Purpose: Human-readable description of the adjustment.

public string GetAdjustmentDescription()
{
var direction = IsPositiveAdjustment ? "increase" : "decrease";
return $"Inventory {direction} at location {LocationId:N}";
}

Output Examples:

  • "Inventory increase at location 3fa85f64-5717-4562-b3fc-2c963f66afa6"
  • "Inventory decrease at location 4gb96g75-6828-5673-c4gd-3d074g77bgb7"

Usage:

  • Audit logs
  • User notifications
  • Transaction history display

Value Object Equality

protected override IEnumerable<object> GetEqualityComponents()
{
yield return LocationId;
yield return IsPositiveAdjustment;
}

Equality Logic: Two AdjustmentDetails are equal if:

  • Same location
  • Same direction (both positive or both negative)

Example:

var adj1 = AdjustmentDetails.Create(locationId, true);
var adj2 = AdjustmentDetails.Create(locationId, true);
var adj3 = AdjustmentDetails.Create(locationId, false);

Assert.That(adj1, Is.EqualTo(adj2)); // True
Assert.That(adj1, Is.EqualTo(adj3)); // False - different direction

Database Persistence

Entity Framework Configuration

builder.OwnsOne(t => t.AdjustmentDetails, ad =>
{
ad.Property(d => d.LocationId)
.HasColumnName("adjustment_location_id")
.IsRequired(false); // Null when Type != Adjustment

ad.Property(d => d.IsPositiveAdjustment)
.HasColumnName("adjustment_is_positive")
.IsRequired(false);
});

Database Schema:

CREATE TABLE inventory_transactions (
id UUID PRIMARY KEY,
type VARCHAR(50) NOT NULL,

-- AdjustmentDetails (nullable)
adjustment_location_id UUID,
adjustment_is_positive BOOLEAN,

FOREIGN KEY (adjustment_location_id) REFERENCES locations(id)
);

Usage Examples

Example 1: Cycle Count - Found Extra Inventory

// Physical count found 150 units, system shows 100
var transaction = InventoryTransaction.CreateAdjustment(
userId: warehouseManager.Id,
locationId: mainWarehouseId,
currentQuantity: 100,
newQuantity: 150,
itemId: widgetItemId,
unitOfMeasureId: eachUnitId,
reason: "Cycle count 2024-Q1 - found additional inventory in back aisle",
externalReference: "CC-2024-001",
notes: "Items were properly stored but not previously recorded");

// AdjustmentDetails: LocationId=mainWarehouseId, IsPositiveAdjustment=true
// Line Item: Quantity=50 (absolute value of adjustment)

Example 2: Cycle Count - Missing Inventory

// Physical count found 95 units, system shows 100
var transaction = InventoryTransaction.CreateAdjustment(
userId: warehouseManager.Id,
locationId: mainWarehouseId,
currentQuantity: 100,
newQuantity: 95,
itemId: widgetItemId,
unitOfMeasureId: eachUnitId,
reason: "Cycle count 2024-Q1 - shrinkage detected",
externalReference: "CC-2024-001",
notes: "Investigating potential theft or miscount");

// AdjustmentDetails: LocationId=mainWarehouseId, IsPositiveAdjustment=false
// Line Item: Quantity=5 (absolute value of adjustment)

Example 3: Damage Write-Off

// 10 units damaged, removing from inventory
var transaction = InventoryTransaction.CreateAdjustment(
userId: qualityControlId,
locationId: inspectionAreaId,
currentQuantity: 100,
newQuantity: 90,
itemId: electronicItemId,
unitOfMeasureId: eachUnitId,
reason: "Water damage during storage - items unusable",
externalReference: "DAMAGE-2024-0045",
notes: "Pipe leak in warehouse section B caused water damage to electronics");

// AdjustmentDetails: LocationId=inspectionAreaId, IsPositiveAdjustment=false
// Line Item: Quantity=10

Example 4: Found Inventory

// Discovered 25 units during facility reorganization
var transaction = InventoryTransaction.CreateAdjustment(
userId: warehouseStaffId,
locationId: storageAreaCId,
currentQuantity: 0,
newQuantity: 25,
itemId: rarePartId,
unitOfMeasureId: eachUnitId,
reason: "Found inventory during warehouse reorganization",
externalReference: "REORG-2024",
notes: "Items found behind old shelving unit, properly sealed and usable");

// AdjustmentDetails: LocationId=storageAreaCId, IsPositiveAdjustment=true
// Line Item: Quantity=25

Testing Strategies

[TestFixture]
public class AdjustmentDetailsTests
{
[Test]
public void Create_ValidInputs_CreatesAdjustmentDetails()
{
// Arrange
var locationId = Guid.NewGuid();

// Act
var details = AdjustmentDetails.Create(locationId, true);

// Assert
Assert.That(details, Is.Not.Null);
Assert.That(details.LocationId, Is.EqualTo(locationId));
Assert.That(details.IsPositiveAdjustment, Is.True);
}

[Test]
public void Create_EmptyLocationId_ThrowsException()
{
// Act & Assert
var ex = Assert.Throws<DomainException>(() =>
AdjustmentDetails.Create(Guid.Empty, true));

Assert.That(ex.Message, Does.Contain("Location ID cannot be empty"));
}

[Test]
public void Create_NegativeAdjustment_SetsCorrectFlag()
{
// Arrange
var locationId = Guid.NewGuid();

// Act
var details = AdjustmentDetails.Create(locationId, false);

// Assert
Assert.That(details.IsPositiveAdjustment, Is.False);
}

[Test]
public void GetAdjustmentDescription_PositiveAdjustment_ReturnsIncrease()
{
// Arrange
var details = AdjustmentDetails.Create(Guid.NewGuid(), true);

// Act
var description = details.GetAdjustmentDescription();

// Assert
Assert.That(description, Does.Contain("increase"));
}

[Test]
public void GetAdjustmentDescription_NegativeAdjustment_ReturnsDecrease()
{
// Arrange
var details = AdjustmentDetails.Create(Guid.NewGuid(), false);

// Act
var description = details.GetAdjustmentDescription();

// Assert
Assert.That(description, Does.Contain("decrease"));
}

[Test]
public void Equality_SameValues_AreEqual()
{
// Arrange
var locationId = Guid.NewGuid();

// Act
var details1 = AdjustmentDetails.Create(locationId, true);
var details2 = AdjustmentDetails.Create(locationId, true);

// Assert
Assert.That(details1, Is.EqualTo(details2));
}

[Test]
public void Equality_DifferentDirection_NotEqual()
{
// Arrange
var locationId = Guid.NewGuid();

// Act
var details1 = AdjustmentDetails.Create(locationId, true);
var details2 = AdjustmentDetails.Create(locationId, false);

// Assert
Assert.That(details1, Is.Not.EqualTo(details2));
}
}

Business Rules Summary

RuleEnforcementRationale
Single location onlyValue object structureAdjustments affect one location
Direction flag requiredProperty (no default)Clear intent for audit and reporting
Location cannot be emptyCreate() validationMust know where adjustment occurred
Zero adjustments rejectedTransaction factoryNo point adjusting to same quantity
Absolute quantities in line itemsTransaction factoryPositive = easier math, direction in flag

Transaction Documentation

Other Value Objects

API Documentation


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

Dependencies: None (Pure value object) Referenced By: InventoryTransaction aggregate Validation: Self-contained within value object