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:
- 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
| Rule | Enforcement | Rationale |
|---|---|---|
| Single location only | Value object structure | Adjustments affect one location |
| Direction flag required | Property (no default) | Clear intent for audit and reporting |
| Location cannot be empty | Create() validation | Must know where adjustment occurred |
| Zero adjustments rejected | Transaction factory | No point adjusting to same quantity |
| Absolute quantities in line items | Transaction factory | Positive = easier math, direction in flag |
Related Documentation
Transaction Documentation
- InventoryTransaction Aggregate - Parent aggregate
- Transaction Types Concept - Business workflows
Other Value Objects
- MovementDetails - Movement type details
- AssemblyDetails - Assembly type details
- SalesDetails - Sales type details
- PurchaseDetails - Purchase type details
API Documentation
- Adjustments API - Adjustment transaction endpoints
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