# Inventory BC - Detailliertes Domain Model

**Bounded Context:** Inventory
**Domain Type:** SUPPORTING
**Verantwortung:** Chargen-basierte Bestandsführung mit FEFO, Reservierungen, Bestandsbewegungen und Inventur

---

## Ubiquitous Language

| Begriff (DE) | Begriff (EN/Code) | Typ | Definition |
|---|---|---|---|
| Bestand | Stock | Aggregate | Aktueller Bestand eines Artikels an einem Lagerort, chargengenau geführt |
| Bestandscharge | StockBatch | Entity | Einzelne Charge im Bestand mit Menge, MHD und Status |
| Chargenreferenz | BatchReference | VO | Verweis auf Produktions- oder Lieferantencharge (ProductionBatchId oder SupplierBatchId) |
| Chargentyp | BatchType | VO (Enum) | PRODUCED (Eigenproduktion) oder PURCHASED (Zukauf) |
| Chargen-Status | StockBatchStatus | VO (Enum) | AVAILABLE, EXPIRING_SOON, BLOCKED, EXPIRED |
| Mindestbestand | MinimumLevel | VO | Mindestmenge, bei Unterschreitung wird Event ausgelöst |
| Mindest-Resthaltbarkeit | MinimumShelfLife | VO | Konfigurierbare Tage bis MHD, ab der Charge als EXPIRING_SOON gilt |
| FEFO | FEFO | Concept | First-Expired-First-Out – Entnahme priorisiert nach nächstem MHD |
| Verfügbarer Bestand | AvailableStock | Concept | Summe aller AVAILABLE-Chargen minus reservierte Mengen |
| Reservierung | Reservation | Entity | Reservierte Menge für einen Produktionsauftrag oder Kundenauftrag |
| Reservierungspriorität | ReservationPriority | VO (Enum) | URGENT, NORMAL, LOW – bestimmt Zuteilungsreihenfolge bei Knappheit |
| Chargen-Zuteilung | StockBatchAllocation | Entity | Zuordnung einer reservierten Menge zu einer konkreten StockBatch (FEFO) |
| Bestandsbewegung | StockMovement | Aggregate | Dokumentierte Bestandsveränderung (Ein-/Ausgang/Umbuchung) |
| Bewegungstyp | MovementType | VO (Enum) | GOODS_RECEIPT, PRODUCTION_OUTPUT, PRODUCTION_CONSUMPTION, SALE, INTER_BRANCH_TRANSFER, WASTE, ADJUSTMENT, RETURN |
| Bewegungsrichtung | MovementDirection | VO (Enum) | IN (Zugang) oder OUT (Abgang) – abgeleitet aus MovementType |
| Inventur | InventoryCount | Aggregate | Physische Bestandsaufnahme mit Soll/Ist-Abgleich und automatischer Differenzbuchung |
| Zählposition | CountItem | Entity | Einzelne Position in einer Inventur: Artikel, Soll-Menge, Ist-Menge, Abweichung |
| Lagerort | StorageLocation | Aggregate | Konfigurierbarer physischer Lagerort mit Typ und Temperaturbereich |
| Lagertyp | StorageType | VO (Enum) | COLD_ROOM, FREEZER, DRY_STORAGE, DISPLAY_COUNTER, PRODUCTION_AREA |
| Temperaturbereich | TemperatureRange | VO | Min-/Max-Temperatur eines Lagerorts in °C |
| Schwund | Shrinkage | Concept | Bestandsverlust durch Verderb, Bruch, Diebstahl – erfasst als WASTE-Bewegung |

---

## Aggregate-Übersicht

```mermaid
---
config:
  theme: neutral
  look: classic
  layout: elk
  themeVariables:
    background: "#f8fafc"
  class:
    hideEmptyMembersBox: true
---
classDiagram
    class Stock {
        +StockId id
        +ArticleId articleId
        +StorageLocationId storageLocationId
        +MinimumLevel minimumLevel
        +MinimumShelfLife minimumShelfLife
        +List~StockBatch~ batches
        +List~Reservation~ reservations
        +create(StockDraft) Result~StockError, Stock~
        +update(StockUpdateDraft) Result~StockError, Void~
        +addBatch(StockBatchDraft) Result~StockError, Void~
        +removeBatch(StockBatchId, Quantity) Result~StockError, Void~
        +blockBatch(StockBatchId, String) Result~StockError, Void~
        +unblockBatch(StockBatchId) Result~StockError, Void~
        +reserve(ReservationDraft) Result~StockError, Void~
        +releaseReservation(ReservationId) Result~StockError, Void~
        +confirmReservation(ReservationId) Result~StockError, Void~
        +markExpiredBatches(LocalDate) Result~StockError, Void~
        +markExpiringSoonBatches(LocalDate) Result~StockError, Void~
        +getAvailableQuantity() Quantity
        +getTotalQuantity() Quantity
    }

    class StockBatch {
        +StockBatchId id
        +BatchReference batchReference
        +Quantity quantity
        +LocalDate expiryDate
        +StockBatchStatus status
        +Instant receivedAt
    }

    class Reservation {
        +ReservationId id
        +ReferenceType referenceType
        +String referenceId
        +Quantity quantity
        +ReservationPriority priority
        +Instant reservedAt
        +List~StockBatchAllocation~ allocations
    }

    class StockBatchAllocation {
        +AllocationId id
        +StockBatchId stockBatchId
        +Quantity allocatedQuantity
    }

    class StockMovement {
        +StockMovementId id
        +StockId stockId
        +ArticleId articleId
        +StockBatchId stockBatchId
        +BatchReference batchReference
        +MovementType movementType
        +MovementDirection direction
        +Quantity quantity
        +String reason
        +String referenceDocumentId
        +UserId performedBy
        +Instant performedAt
        +record(StockMovementDraft) Result~StockMovementError, StockMovement~
    }

    class InventoryCount {
        +InventoryCountId id
        +StorageLocationId storageLocationId
        +LocalDate countDate
        +InventoryCountStatus status
        +UserId initiatedBy
        +UserId completedBy
        +Instant completedAt
        +List~CountItem~ countItems
        +create(InventoryCountDraft) Result~InventoryCountError, InventoryCount~
        +addCountItem(CountItemDraft) Result~InventoryCountError, Void~
        +updateCountItem(CountItemId, Quantity) Result~InventoryCountError, Void~
        +complete(UserId) Result~InventoryCountError, Void~
        +cancel(String) Result~InventoryCountError, Void~
    }

    class CountItem {
        +CountItemId id
        +ArticleId articleId
        +Quantity expectedQuantity
        +Quantity actualQuantity
        +Quantity deviation
    }

    class StorageLocation {
        +StorageLocationId id
        +StorageLocationName name
        +StorageType storageType
        +TemperatureRange temperatureRange
        +boolean active
        +create(StorageLocationDraft) Result~StorageLocationError, StorageLocation~
        +update(StorageLocationUpdateDraft) Result~StorageLocationError, Void~
        +deactivate() Result~StorageLocationError, Void~
        +activate() Result~StorageLocationError, Void~
    }

    Stock "1" *-- "*" StockBatch : enthält
    Stock "1" *-- "*" Reservation : enthält
    Reservation "1" *-- "*" StockBatchAllocation : enthält
    InventoryCount "1" *-- "*" CountItem : enthält

    Stock ..> StockId
    Stock ..> ArticleId
    Stock ..> StorageLocationId
    Stock ..> MinimumLevel
    Stock ..> MinimumShelfLife

    StockBatch ..> StockBatchId
    StockBatch ..> BatchReference
    StockBatch ..> StockBatchStatus
    StockBatch ..> Quantity

    StockBatchAllocation ..> AllocationId

    StockMovement ..> StockMovementId
    StockMovement ..> MovementType
    StockMovement ..> MovementDirection

    InventoryCount ..> InventoryCountId
    InventoryCount ..> InventoryCountStatus

    StorageLocation ..> StorageLocationId
    StorageLocation ..> StorageLocationName
    StorageLocation ..> StorageType
    StorageLocation ..> TemperatureRange
```

---

## Aggregates

### 1. Stock (Aggregate Root)

**Verantwortung:** Verwaltet den chargen-genauen Bestand eines Artikels an einem Lagerort. Erzwingt FEFO-Prinzip bei Reservierungen und prüft Mindestbestände.

```
Stock (Aggregate Root)
├── StockId (VO)
├── ArticleId (VO) - Referenz auf Stammdaten-Artikel
├── StorageLocationId (VO) - Referenz auf Lagerort
├── MinimumLevel (VO) - Mindestbestand für Nachbestellwarnung
├── MinimumShelfLife (VO, optional) - Tage bis MHD für EXPIRING_SOON
├── StockBatches[] (Entity)
│   ├── StockBatchId (VO)
│   ├── BatchReference (VO)
│   │   ├── batchId (String) - ProductionBatchId oder SupplierBatchId
│   │   └── batchType (BatchType: PRODUCED | PURCHASED)
│   ├── Quantity (VO) - Aktuelle Menge (mit Catch-Weight)
│   ├── ExpiryDate (LocalDate) - MHD
│   ├── Status (StockBatchStatus: AVAILABLE | EXPIRING_SOON | BLOCKED | EXPIRED)
│   └── ReceivedAt (Instant) - Einbuchungszeitpunkt
└── Reservations[] (Entity)
    ├── ReservationId (VO)
    ├── ReferenceType (VO: PRODUCTION_ORDER | SALE_ORDER)
    ├── ReferenceId (String) - ID des referenzierten Auftrags
    ├── Quantity (VO) - Reservierte Gesamtmenge
    ├── Priority (ReservationPriority: URGENT | NORMAL | LOW)
    ├── ReservedAt (Instant)
    └── StockBatchAllocations[] (Entity)
        ├── AllocationId (VO)
        ├── StockBatchId (VO) - Zugeordnete Charge
        └── AllocatedQuantity (VO) - Reservierte Menge aus dieser Charge
```

**Invarianten:**
```java
/**
 * Stock aggregate root.
 *
 * Invariants:
 * - ArticleId + StorageLocationId bilden ein logisches Unique-Constraint (Application Layer)
 * - StockBatch.Quantity must be positive (> 0)
 * - Negative Gesamtbestände sind nicht erlaubt
 * - FEFO: Reservierungen allokieren immer die Chargen mit dem nächsten MHD zuerst
 * - AvailableQuantity = SUM(AVAILABLE batches) - SUM(allocated reservations)
 * - AvailableQuantity must be >= 0 (cannot over-reserve)
 * - Expired batches (ExpiryDate < today) must have Status = EXPIRED
 * - EXPIRING_SOON batches: ExpiryDate < today + MinimumShelfLife.days
 * - BLOCKED batches cannot be reserved or withdrawn
 * - StockBatchAllocations.SUM(allocatedQuantity) must equal Reservation.quantity
 * - Each StockBatchAllocation must reference an AVAILABLE or EXPIRING_SOON batch
 * - MinimumLevel.amount must be >= 0
 * - BatchReference must be unique within a Stock (no duplicate batch entries)
 */
```

**Draft-Records:**
```java
public record StockDraft(
    String articleId,
    String storageLocationId,
    String minimumLevelAmount,       // BigDecimal als String, nullable
    String minimumLevelUnit,         // UnitOfMeasure, nullable
    Integer minimumShelfLifeDays     // nullable
) {}

public record StockUpdateDraft(
    String minimumLevelAmount,       // BigDecimal als String, nullable
    String minimumLevelUnit,         // UnitOfMeasure, nullable
    Integer minimumShelfLifeDays     // nullable
) {}

public record StockBatchDraft(
    String batchId,                  // ProductionBatchId oder SupplierBatchId
    String batchType,                // PRODUCED | PURCHASED
    String quantityAmount,           // BigDecimal als String
    String quantityUnit,             // UnitOfMeasure
    String expiryDate                // ISO LocalDate
) {}

public record ReservationDraft(
    String referenceType,            // PRODUCTION_ORDER | SALE_ORDER
    String referenceId,
    String quantityAmount,           // BigDecimal als String
    String quantityUnit,             // UnitOfMeasure
    String priority                  // URGENT | NORMAL | LOW
) {}
```

**Factory & Business Methods:**
```java
// Factory
public static Result<StockError, Stock> create(StockDraft draft);

// Bestandsänderungen
public Result<StockError, Void> update(StockUpdateDraft draft);
public Result<StockError, Void> addBatch(StockBatchDraft draft);
public Result<StockError, Void> removeBatch(StockBatchId batchId, Quantity quantity);
public Result<StockError, Void> blockBatch(StockBatchId batchId, String reason);
public Result<StockError, Void> unblockBatch(StockBatchId batchId);

// Reservierungen (FEFO-basiert)
public Result<StockError, Void> reserve(ReservationDraft draft);
public Result<StockError, Void> releaseReservation(ReservationId reservationId);
public Result<StockError, Void> confirmReservation(ReservationId reservationId);

// MHD-Prüfung (typischerweise per Scheduler aufgerufen)
public Result<StockError, Void> markExpiredBatches(LocalDate today);
public Result<StockError, Void> markExpiringSoonBatches(LocalDate today);

// Query Methods
public Quantity getAvailableQuantity();
public Quantity getTotalQuantity();
public List<StockBatch> getAvailableBatchesByFefo();
public boolean isBelowMinimumLevel();
```

**Domain Events:**
```java
StockBatchAdded(StockId, StockBatchId, ArticleId, BatchReference, Quantity, LocalDate expiryDate)
StockBatchRemoved(StockId, StockBatchId, Quantity)
StockBatchBlocked(StockId, StockBatchId, String reason)
StockBatchUnblocked(StockId, StockBatchId)
StockReserved(StockId, ReservationId, Quantity, String referenceId)
StockReservationReleased(StockId, ReservationId)
StockReservationConfirmed(StockId, ReservationId, Quantity)
StockLevelBelowMinimum(StockId, ArticleId, StorageLocationId, Quantity currentLevel, Quantity minimumLevel)
  → triggers Procurement demand / Notification
BatchExpiringSoon(StockId, StockBatchId, ArticleId, LocalDate expiryDate, int daysRemaining)
  → triggers Notification
BatchExpired(StockId, StockBatchId, ArticleId, LocalDate expiryDate)
```

---

### 2. StockMovement (Aggregate Root)

**Verantwortung:** Dokumentiert jede Bestandsveränderung als unveränderlichen Audit-Trail. Ermöglicht lückenlose Rückverfolgbarkeit aller Ein- und Ausgänge.

```
StockMovement (Aggregate Root)
├── StockMovementId (VO)
├── StockId (VO) - Referenz auf betroffenen Stock
├── ArticleId (VO) - Denormalisiert für einfache Abfragen
├── StockBatchId (VO) - Betroffene Charge
├── BatchReference (VO) - Chargenreferenz (denormalisiert)
├── MovementType (VO: GOODS_RECEIPT | PRODUCTION_OUTPUT | PRODUCTION_CONSUMPTION |
│                      SALE | INTER_BRANCH_TRANSFER | WASTE | ADJUSTMENT | RETURN)
├── Direction (VO: IN | OUT) - Abgeleitet aus MovementType
├── Quantity (VO) - Bewegte Menge (immer positiv)
├── Reason (String, optional) - Pflicht bei WASTE und ADJUSTMENT
├── ReferenceDocumentId (String, optional) - GoodsReceiptId, ProductionOrderId, InvoiceId etc.
├── PerformedBy (VO: UserId)
└── PerformedAt (VO: Instant)
```

**Invarianten:**
```java
/**
 * StockMovement aggregate root (immutable after creation).
 *
 * Invariants:
 * - Quantity must be positive
 * - Direction is derived: IN for GOODS_RECEIPT, PRODUCTION_OUTPUT, RETURN, ADJUSTMENT(+)
 *                         OUT for PRODUCTION_CONSUMPTION, SALE, WASTE, ADJUSTMENT(-), INTER_BRANCH_TRANSFER(source)
 * - WASTE and ADJUSTMENT require a non-empty Reason
 * - INTER_BRANCH_TRANSFER must have a ReferenceDocumentId (Transfer-Dokument)
 * - PerformedBy must not be null
 * - StockMovement is immutable after creation (append-only)
 * - BatchReference must not be null (all movements are batch-traceable)
 */
```

**Draft-Record:**
```java
public record StockMovementDraft(
    String stockId,
    String articleId,
    String stockBatchId,
    String batchId,                  // BatchReference.batchId
    String batchType,                // BatchReference.batchType
    String movementType,             // MovementType enum
    String quantityAmount,           // BigDecimal als String
    String quantityUnit,             // UnitOfMeasure
    String reason,                   // nullable, Pflicht bei WASTE/ADJUSTMENT
    String referenceDocumentId,      // nullable
    String performedBy               // UserId
) {}
```

**Factory & Business Methods:**
```java
// Factory (einzige Mutation – StockMovement ist immutable)
public static Result<StockMovementError, StockMovement> record(StockMovementDraft draft);

// Query Methods
public boolean isIncoming();
public boolean isOutgoing();
```

**Domain Events:**
```java
StockMovementRecorded(StockMovementId, StockId, ArticleId, BatchReference, MovementType, Quantity)
```

---

### 3. InventoryCount (Aggregate Root)

**Verantwortung:** Physische Bestandsaufnahme pro Lagerort mit Soll/Ist-Abgleich. Abweichungen werden nach Abschluss als ADJUSTMENT-StockMovements verbucht.

```
InventoryCount (Aggregate Root)
├── InventoryCountId (VO)
├── StorageLocationId (VO) - Gezählter Lagerort
├── CountDate (LocalDate) - Stichtag der Inventur
├── Status (InventoryCountStatus: OPEN | COUNTING | COMPLETED | CANCELLED)
├── InitiatedBy (VO: UserId)
├── CompletedBy (VO: UserId, optional)
├── CompletedAt (Instant, optional)
├── CancelledReason (String, optional)
└── CountItems[] (Entity)
    ├── CountItemId (VO)
    ├── ArticleId (VO) - Gezählter Artikel
    ├── ExpectedQuantity (VO: Quantity) - Soll-Bestand aus System
    ├── ActualQuantity (VO: Quantity, optional) - Ist-Menge nach Zählung
    └── Deviation (VO: Quantity, optional) - Berechnet: ActualQuantity - ExpectedQuantity
```

**Invarianten:**
```java
/**
 * InventoryCount aggregate root.
 *
 * Invariants:
 * - Status transitions: OPEN → COUNTING → COMPLETED
 *                       OPEN → CANCELLED
 *                       COUNTING → CANCELLED
 * - Cannot complete without at least one CountItem
 * - All CountItems must have ActualQuantity set before completion
 * - Deviation is auto-calculated: ActualQuantity - ExpectedQuantity
 * - CountDate cannot be in the future at creation
 * - ArticleId must be unique within CountItems (no duplicate articles)
 * - Only one active (OPEN/COUNTING) InventoryCount per StorageLocation (Application Layer)
 * - CompletedBy must differ from InitiatedBy (Vier-Augen-Prinzip)
 */
```

**Draft-Records:**
```java
public record InventoryCountDraft(
    String storageLocationId,
    String countDate,                // ISO LocalDate
    String initiatedBy               // UserId
) {}

public record CountItemDraft(
    String articleId,
    String expectedQuantityAmount,   // BigDecimal als String
    String expectedQuantityUnit      // UnitOfMeasure
) {}
```

**Factory & Business Methods:**
```java
// Factory
public static Result<InventoryCountError, InventoryCount> create(InventoryCountDraft draft);

// Zählung
public Result<InventoryCountError, Void> startCounting();
public Result<InventoryCountError, Void> addCountItem(CountItemDraft draft);
public Result<InventoryCountError, Void> updateCountItem(CountItemId itemId, Quantity actualQuantity);

// Abschluss
public Result<InventoryCountError, Void> complete(UserId completedBy);
public Result<InventoryCountError, Void> cancel(String reason);

// Query Methods
public List<CountItem> getDeviations();
public boolean hasDeviations();
public Quantity getTotalDeviation();
```

**Domain Events:**
```java
InventoryCountCreated(InventoryCountId, StorageLocationId, LocalDate countDate)
InventoryCountStarted(InventoryCountId)
InventoryCountCompleted(InventoryCountId, StorageLocationId, List<CountItem> deviations)
  → triggers ADJUSTMENT StockMovements for each deviation
InventoryCountCancelled(InventoryCountId, String reason)
```

---

### 4. StorageLocation (Aggregate Root)

**Verantwortung:** Verwaltet konfigurierbare physische Lagerorte mit Typ-Klassifizierung und optionalem Temperaturbereich.

```
StorageLocation (Aggregate Root)
├── StorageLocationId (VO)
├── Name (VO: StorageLocationName) - Eindeutiger Name
├── StorageType (VO: COLD_ROOM | FREEZER | DRY_STORAGE | DISPLAY_COUNTER | PRODUCTION_AREA)
├── TemperatureRange (VO, optional)
│   ├── MinTemperature (BigDecimal) - in °C
│   └── MaxTemperature (BigDecimal) - in °C
└── Active (boolean) - Soft-Delete
```

**Invarianten:**
```java
/**
 * StorageLocation aggregate root.
 *
 * Invariants:
 * - Name must be unique (Application Layer, Repository-Concern)
 * - Name must not be blank, max 100 chars
 * - TemperatureRange: minTemperature < maxTemperature
 * - TemperatureRange: values must be in range -50°C to +80°C
 * - Cannot deactivate if Stock exists at this location (Application Layer)
 * - StorageType is immutable after creation (changing type = new location)
 */
```

**Draft-Records:**
```java
public record StorageLocationDraft(
    String name,
    String storageType,              // StorageType enum
    String minTemperature,           // BigDecimal als String, nullable
    String maxTemperature            // BigDecimal als String, nullable
) {}

public record StorageLocationUpdateDraft(
    String name,                     // nullable = no change
    String minTemperature,           // BigDecimal als String, nullable
    String maxTemperature            // BigDecimal als String, nullable
) {}
```

**Factory & Business Methods:**
```java
// Factory
public static Result<StorageLocationError, StorageLocation> create(StorageLocationDraft draft);

// Mutations
public Result<StorageLocationError, Void> update(StorageLocationUpdateDraft draft);
public Result<StorageLocationError, Void> deactivate();
public Result<StorageLocationError, Void> activate();

// Query Methods
public boolean isTemperatureControlled();
```

**Domain Events:**
```java
StorageLocationCreated(StorageLocationId, String name, StorageType)
StorageLocationDeactivated(StorageLocationId)
```

---

## Shared Value Objects

```mermaid
---
config:
  theme: neutral
  look: classic
  layout: elk
  themeVariables:
    background: "#f8fafc"
  class:
    hideEmptyMembersBox: true
---
classDiagram
    class BatchReference {
        +String batchId
        +BatchType batchType
        +of(String, BatchType) Result
    }

    class BatchType {
        <<enumeration>>
        PRODUCED
        PURCHASED
    }

    class MinimumLevel {
        +Quantity quantity
        +of(Quantity) Result
    }

    class MinimumShelfLife {
        +int days
        +of(int) Result
        +isExpiringSoon(LocalDate, LocalDate) boolean
    }

    class StockBatchStatus {
        <<enumeration>>
        AVAILABLE
        EXPIRING_SOON
        BLOCKED
        EXPIRED
    }

    class ReservationPriority {
        <<enumeration>>
        URGENT
        NORMAL
        LOW
    }

    class ReferenceType {
        <<enumeration>>
        PRODUCTION_ORDER
        SALE_ORDER
    }

    class MovementType {
        <<enumeration>>
        GOODS_RECEIPT
        PRODUCTION_OUTPUT
        PRODUCTION_CONSUMPTION
        SALE
        INTER_BRANCH_TRANSFER
        WASTE
        ADJUSTMENT
        RETURN
    }

    class MovementDirection {
        <<enumeration>>
        IN
        OUT
    }

    class InventoryCountStatus {
        <<enumeration>>
        OPEN
        COUNTING
        COMPLETED
        CANCELLED
    }

    class StorageType {
        <<enumeration>>
        COLD_ROOM
        FREEZER
        DRY_STORAGE
        DISPLAY_COUNTER
        PRODUCTION_AREA
    }

    class StorageLocationName {
        +String value
        +of(String) Result
    }

    class TemperatureRange {
        +BigDecimal minTemperature
        +BigDecimal maxTemperature
        +of(BigDecimal, BigDecimal) Result
        +contains(BigDecimal) boolean
    }

    BatchReference --> BatchType
    MovementType --> MovementDirection : derives
```

### BatchReference

```java
public record BatchReference(String batchId, BatchType batchType) {
    public static Result<StockError, BatchReference> of(String batchId, String batchType) {
        // batchId must not be blank
        // batchType must be valid enum value
    }
}
```

### MinimumLevel

```java
public record MinimumLevel(Quantity quantity) {
    public static Result<StockError, MinimumLevel> of(Quantity quantity) {
        // quantity.amount must be >= 0
    }

    public boolean isBelow(Quantity currentLevel) {
        return currentLevel.amount().compareTo(quantity.amount()) < 0;
    }
}
```

### MinimumShelfLife

```java
public record MinimumShelfLife(int days) {
    public static Result<StockError, MinimumShelfLife> of(int days) {
        // days must be > 0
    }

    public boolean isExpiringSoon(LocalDate expiryDate, LocalDate today) {
        return expiryDate.isBefore(today.plusDays(days));
    }
}
```

### TemperatureRange

```java
public record TemperatureRange(BigDecimal minTemperature, BigDecimal maxTemperature) {
    public static Result<StorageLocationError, TemperatureRange> of(
            BigDecimal minTemperature, BigDecimal maxTemperature) {
        // minTemperature < maxTemperature
        // both in range -50 to +80
    }

    public boolean contains(BigDecimal temperature) {
        return temperature.compareTo(minTemperature) >= 0
            && temperature.compareTo(maxTemperature) <= 0;
    }
}
```

### StorageLocationName

```java
public record StorageLocationName(String value) {
    public static Result<StorageLocationError, StorageLocationName> of(String value) {
        // must not be blank
        // max 100 chars
    }
}
```

### MovementType

```java
public enum MovementType {
    GOODS_RECEIPT(MovementDirection.IN),
    PRODUCTION_OUTPUT(MovementDirection.IN),
    PRODUCTION_CONSUMPTION(MovementDirection.OUT),
    SALE(MovementDirection.OUT),
    INTER_BRANCH_TRANSFER(MovementDirection.OUT),  // Source-Seite
    WASTE(MovementDirection.OUT),
    ADJUSTMENT(null),  // Richtung wird durch Vorzeichen der Menge bestimmt
    RETURN(MovementDirection.IN);

    private final MovementDirection defaultDirection;
}
```

### ReservationPriority

```java
public enum ReservationPriority {
    URGENT(1),
    NORMAL(2),
    LOW(3);

    private final int sortOrder;
}
```

---

## Domain Services

### StockExpiryChecker

```java
/**
 * Prüft alle Stocks auf ablaufende und abgelaufene Chargen.
 * Wird typischerweise per Scheduler (z.B. täglich um 06:00) aufgerufen.
 */
public class StockExpiryChecker {
    /**
     * Markiert abgelaufene Chargen als EXPIRED und
     * bald ablaufende als EXPIRING_SOON.
     * Löst BatchExpiringSoon und BatchExpired Events aus.
     */
    public void checkAll(LocalDate today, StockRepository stockRepository);
}
```

### InventoryCountReconciliationService

```java
/**
 * Erstellt ADJUSTMENT-StockMovements für Inventur-Abweichungen
 * nach Abschluss einer InventoryCount.
 */
public class InventoryCountReconciliationService {
    /**
     * Für jede CountItem-Abweichung wird ein StockMovement (ADJUSTMENT) erzeugt.
     * Positive Abweichung → IN, negative Abweichung → OUT.
     */
    public List<StockMovement> reconcile(InventoryCount completedCount, UserId performedBy);
}
```

### StockAvailabilityService

```java
/**
 * Prüft Materialverfügbarkeit für Produktionsaufträge.
 * Wird vom Production BC aufgerufen (via Application Layer).
 */
public class StockAvailabilityService {
    /**
     * Prüft ob ausreichend Material an einem Lagerort verfügbar ist.
     */
    public boolean isAvailable(ArticleId articleId, StorageLocationId locationId, Quantity requiredQuantity);

    /**
     * Gibt verfügbare Menge über alle Lagerorte zurück.
     */
    public Quantity getAvailableQuantityAcrossLocations(ArticleId articleId);
}
```

---

## Domain Errors

```java
public sealed interface StockError {
    String message();

    record StockNotFound(String stockId) implements StockError {
        public String message() { return "Stock not found: " + stockId; }
    }
    record DuplicateBatchReference(String batchId) implements StockError {
        public String message() { return "Batch already exists in stock: " + batchId; }
    }
    record InsufficientStock(String available, String requested) implements StockError {
        public String message() { return "Insufficient stock: available=" + available + ", requested=" + requested; }
    }
    record BatchNotFound(String stockBatchId) implements StockError {
        public String message() { return "Stock batch not found: " + stockBatchId; }
    }
    record BatchBlocked(String stockBatchId) implements StockError {
        public String message() { return "Batch is blocked: " + stockBatchId; }
    }
    record BatchExpired(String stockBatchId) implements StockError {
        public String message() { return "Batch is expired: " + stockBatchId; }
    }
    record ReservationNotFound(String reservationId) implements StockError {
        public String message() { return "Reservation not found: " + reservationId; }
    }
    record InvalidQuantity(String reason) implements StockError {
        public String message() { return "Invalid quantity: " + reason; }
    }
    record InvalidMinimumLevel(String reason) implements StockError {
        public String message() { return "Invalid minimum level: " + reason; }
    }
    record InvalidMinimumShelfLife(int days) implements StockError {
        public String message() { return "Minimum shelf life must be > 0, got: " + days; }
    }
    record InvalidBatchReference(String reason) implements StockError {
        public String message() { return "Invalid batch reference: " + reason; }
    }
    record NegativeStockNotAllowed() implements StockError {
        public String message() { return "Stock cannot go negative"; }
    }
    record InvalidReferenceType(String value) implements StockError {
        public String message() { return "Invalid reference type: " + value; }
    }
    record InvalidReferenceId(String reason) implements StockError {
        public String message() { return "Invalid reference ID: " + reason; }
    }
    record InvalidReservationPriority(String value) implements StockError {
        public String message() { return "Invalid reservation priority: " + value; }
    }
}

public sealed interface StockMovementError {
    String message();

    record InvalidQuantity(String reason) implements StockMovementError {
        public String message() { return "Invalid quantity: " + reason; }
    }
    record ReasonRequired(String movementType) implements StockMovementError {
        public String message() { return "Reason is required for movement type: " + movementType; }
    }
    record ReferenceDocumentRequired(String movementType) implements StockMovementError {
        public String message() { return "Reference document required for: " + movementType; }
    }
    record InvalidMovementType(String value) implements StockMovementError {
        public String message() { return "Invalid movement type: " + value; }
    }
    record MissingBatchReference() implements StockMovementError {
        public String message() { return "All stock movements must reference a batch"; }
    }
}

public sealed interface InventoryCountError {
    String message();

    record InvalidStatusTransition(String from, String to) implements InventoryCountError {
        public String message() { return "Cannot transition from " + from + " to " + to; }
    }
    record CountDateInFuture(String date) implements InventoryCountError {
        public String message() { return "Count date cannot be in the future: " + date; }
    }
    record NoCountItems() implements InventoryCountError {
        public String message() { return "Cannot complete inventory count without count items"; }
    }
    record IncompleteCountItems(int uncounted) implements InventoryCountError {
        public String message() { return "Cannot complete: " + uncounted + " items not yet counted"; }
    }
    record DuplicateArticle(String articleId) implements InventoryCountError {
        public String message() { return "Article already in count: " + articleId; }
    }
    record CountItemNotFound(String countItemId) implements InventoryCountError {
        public String message() { return "Count item not found: " + countItemId; }
    }
    record SamePersonViolation() implements InventoryCountError {
        public String message() { return "Initiator and completer must be different persons (Vier-Augen-Prinzip)"; }
    }
}

public sealed interface StorageLocationError {
    String message();

    record NameAlreadyExists(String name) implements StorageLocationError {
        public String message() { return "Storage location name already exists: " + name; }
    }
    record InvalidName(String reason) implements StorageLocationError {
        public String message() { return "Invalid storage location name: " + reason; }
    }
    record InvalidTemperatureRange(String reason) implements StorageLocationError {
        public String message() { return "Invalid temperature range: " + reason; }
    }
    record StockExistsAtLocation(String locationId) implements StorageLocationError {
        public String message() { return "Cannot deactivate: stock exists at location " + locationId; }
    }
    record AlreadyActive() implements StorageLocationError {
        public String message() { return "Storage location is already active"; }
    }
    record AlreadyInactive() implements StorageLocationError {
        public String message() { return "Storage location is already inactive"; }
    }
}
```

---

## Repository Interfaces

```java
public interface StockRepository {
    Optional<Stock> findById(StockId id);
    Optional<Stock> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId locationId);
    List<Stock> findByArticleId(ArticleId articleId);
    List<Stock> findByStorageLocationId(StorageLocationId locationId);
    List<Stock> findBelowMinimumLevel();
    List<Stock> findWithExpiringBatches(LocalDate before);
    List<Stock> findByBatchReference(BatchReference batchReference);
    void save(Stock stock);
}

public interface StockMovementRepository {
    Optional<StockMovement> findById(StockMovementId id);
    List<StockMovement> findByStockId(StockId stockId);
    List<StockMovement> findByArticleId(ArticleId articleId);
    List<StockMovement> findByBatchReference(BatchReference batchReference);
    List<StockMovement> findByMovementType(MovementType type);
    List<StockMovement> findByPerformedAtBetween(Instant from, Instant to);
    void save(StockMovement movement);
}

public interface InventoryCountRepository {
    Optional<InventoryCount> findById(InventoryCountId id);
    List<InventoryCount> findByStorageLocationId(StorageLocationId locationId);
    List<InventoryCount> findByStatus(InventoryCountStatus status);
    List<InventoryCount> findByCountDateBetween(LocalDate from, LocalDate to);
    boolean existsActiveByStorageLocationId(StorageLocationId locationId);
    void save(InventoryCount count);
}

public interface StorageLocationRepository {
    Optional<StorageLocation> findById(StorageLocationId id);
    List<StorageLocation> findAll();
    List<StorageLocation> findByStorageType(StorageType type);
    List<StorageLocation> findActive();
    boolean existsByName(StorageLocationName name);
    void save(StorageLocation location);
}
```

---

## Status-Maschinen

### StockBatch Status

```mermaid
---
config:
  theme: neutral
  themeVariables:
    background: "#f8fafc"
---
stateDiagram-v2
    [*] --> AVAILABLE : addBatch()
    AVAILABLE --> EXPIRING_SOON : markExpiringSoonBatches()
    AVAILABLE --> BLOCKED : blockBatch()
    AVAILABLE --> EXPIRED : markExpiredBatches()
    EXPIRING_SOON --> BLOCKED : blockBatch()
    EXPIRING_SOON --> EXPIRED : markExpiredBatches()
    BLOCKED --> AVAILABLE : unblockBatch()
    BLOCKED --> EXPIRING_SOON : unblockBatch() + MHD-Check

    AVAILABLE : Frei verfügbar, reservierbar
    EXPIRING_SOON : MHD < heute + MinimumShelfLife
    EXPIRING_SOON : Noch verfügbar, aber Warnung
    BLOCKED : Gesperrt (QualityHold etc.)
    BLOCKED : Nicht reservierbar/entnehmbar
    EXPIRED : MHD überschritten
    EXPIRED : Muss als WASTE verbucht werden
```

### InventoryCount Status

```mermaid
---
config:
  theme: neutral
  themeVariables:
    background: "#f8fafc"
---
stateDiagram-v2
    [*] --> OPEN : create()
    OPEN --> COUNTING : startCounting()
    OPEN --> CANCELLED : cancel()
    COUNTING --> COMPLETED : complete()
    COUNTING --> CANCELLED : cancel()

    OPEN : Inventur angelegt
    OPEN : addCountItem() möglich
    COUNTING : Zählung läuft
    COUNTING : updateCountItem() möglich
    COMPLETED : Alle Positionen gezählt
    COMPLETED : Abweichungen als ADJUSTMENT gebucht
    CANCELLED : Inventur abgebrochen
```

---

## Integration mit anderen BCs

```mermaid
---
config:
  theme: neutral
  look: classic
  layout: elk
  themeVariables:
    background: "#f8fafc"
---
graph LR
    subgraph UPSTREAM["Upstream BCs"]
        MD["Master Data\n(ArticleId)"]
        UM["User Management\n(UserId)"]
        PROD["Production BC"]
        QUAL["Quality BC"]
        PROC["Procurement BC"]
    end

    subgraph INVENTORY["Inventory BC"]
        S["Stock"]
        SM["StockMovement"]
        IC["InventoryCount"]
        SL["StorageLocation"]
    end

    subgraph DOWNSTREAM["Downstream BCs"]
        SALES["Sales BC"]
        PROCDOWN["Procurement BC\n(Nachbestellung)"]
    end

    MD -->|ArticleId| S
    UM -->|UserId| SM
    UM -->|UserId| IC

    PROD -->|"BatchCompleted\n(stock-in)"| S
    PROD -->|"ConsumptionRecorded\n(stock-out)"| S
    QUAL -->|"QualityHoldCreated\n(block batch)"| S
    QUAL -->|"QualityHoldReleased\n(unblock batch)"| S
    QUAL -->|"QualityHoldRejected\n(write-off)"| S
    PROC -->|"GoodsReceived\n(stock-in)"| S

    S -->|"StockLevelBelowMinimum\n(demand)"| PROCDOWN
    S -->|"BatchExpiringSoon\n(notification)"| SALES
```

### Upstream-Abhängigkeiten (Inventory konsumiert)
| BC | Referenz / Event | Zweck |
|---|---|---|
| **Master Data** | ArticleId | Artikel-Referenz in Stock, StockMovement, CountItem |
| **User Management** | UserId | PerformedBy in StockMovement, InitiatedBy/CompletedBy in InventoryCount |
| **Production** | `BatchCompleted` Event | Stock-In: Neue StockBatch mit BatchType=PRODUCED |
| **Production** | `ConsumptionRecorded` Event | Stock-Out: removeBatch() für verbrauchte Rohstoffe |
| **Quality** | `QualityHoldCreated` Event | blockBatch() – Charge sperren |
| **Quality** | `QualityHoldReleased` Event | unblockBatch() – Charge freigeben |
| **Quality** | `QualityHoldRejected` Event | removeBatch() + WASTE-StockMovement – Charge entsorgen |
| **Procurement** | `GoodsReceived` Event | Stock-In: Neue StockBatch mit BatchType=PURCHASED |

### Downstream-Integrationen (Inventory publiziert Events)
| Event | Konsument | Aktion |
|---|---|---|
| `StockLevelBelowMinimum` | **Procurement BC** | Automatische Nachbestellung / Bestellvorschlag |
| `BatchExpiringSoon` | **Sales BC** | Rabattaktion / Prioritäts-Verkauf |
| `StockMovementRecorded` | **Audit** | Lückenloser Audit-Trail |
| `InventoryCountCompleted` | **Audit** | Inventur-Dokumentation |

### Abgrenzungen (gehören NICHT in Inventory BC)
| Konzept | Zuständiger BC | Grund |
|---|---|---|
| Qualitätssperre (QualityHold) | **Quality BC** | Eigene Aggregate-Logik mit Vier-Augen-Prinzip |
| Rezeptur / Produktion | **Production BC** | Inventory kennt keine Rezepte, nur Mengen |
| Lieferant / Wareneingang | **Procurement BC** | Inventory reagiert nur auf GoodsReceived Event |
| Artikelstamm | **Master Data BC** | Inventory referenziert nur ArticleId |
| Preisgestaltung | **Sales BC** | Inventory verwaltet keine Preise |

---

## Use Cases (Application Layer)

```java
// Stock Management
CreateStock              → Stock.create(StockDraft)
UpdateStock              → stock.update(StockUpdateDraft)
AddStockBatch            → stock.addBatch(StockBatchDraft) + StockMovement.record()
RemoveStockBatch         → stock.removeBatch(id, qty) + StockMovement.record()
BlockStockBatch          → stock.blockBatch(id, reason)
UnblockStockBatch        → stock.unblockBatch(id)
GetStock                 → Query
ListStockByLocation      → Query
ListStockBelowMinimum    → Query

// Reservations
ReserveStock             → stock.reserve(ReservationDraft)
ReleaseReservation       → stock.releaseReservation(reservationId)
ConfirmReservation       → stock.confirmReservation(reservationId) + StockMovement.record()

// Stock Movements
RecordStockMovement      → StockMovement.record(StockMovementDraft)
GetStockMovement         → Query
ListMovementsByArticle   → Query
ListMovementsByPeriod    → Query

// Inventory Count
CreateInventoryCount     → InventoryCount.create(InventoryCountDraft) + auto-populate CountItems
StartInventoryCount      → count.startCounting()
RecordCountItem          → count.updateCountItem(id, actualQuantity)
CompleteInventoryCount   → count.complete(userId) + reconciliation → ADJUSTMENT movements
CancelInventoryCount     → count.cancel(reason)
GetInventoryCount        → Query

// Storage Location Management
CreateStorageLocation    → StorageLocation.create(StorageLocationDraft)
UpdateStorageLocation    → location.update(StorageLocationUpdateDraft)
DeactivateStorageLocation → location.deactivate()
ListStorageLocations     → Query

// Event Handlers (reagieren auf Upstream-Events)
HandleBatchCompleted         → AddStockBatch (PRODUCED)
HandleConsumptionRecorded    → RemoveStockBatch
HandleGoodsReceived          → AddStockBatch (PURCHASED)
HandleQualityHoldCreated     → BlockStockBatch
HandleQualityHoldReleased    → UnblockStockBatch
HandleQualityHoldRejected    → RemoveStockBatch + WASTE movement
```

---

## Beispiel: Bestandsfluss (End-to-End)

```mermaid
---
config:
  theme: neutral
  themeVariables:
    background: "#f8fafc"
---
sequenceDiagram
    participant PROC as Procurement BC
    participant INV as Inventory (Stock)
    participant SM as StockMovement
    participant PROD as Production BC
    participant QUAL as Quality BC
    participant SALES as Sales BC

    Note over PROC,INV: 1. Wareneingang
    PROC--)INV: GoodsReceived Event
    activate INV
    INV->>INV: addBatch(PURCHASED, 100kg Schweineschulter, MHD 2026-03-15)
    INV->>SM: record(GOODS_RECEIPT, 100kg)
    deactivate INV

    Note over PROD,INV: 2. Produktions-Verbrauch
    PROD--)INV: ConsumptionRecorded Event
    activate INV
    INV->>INV: removeBatch(Schweineschulter, 45kg)
    INV->>SM: record(PRODUCTION_CONSUMPTION, 45kg)
    deactivate INV

    Note over PROD,INV: 3. Produktions-Zugang
    PROD--)INV: BatchCompleted Event
    activate INV
    INV->>INV: addBatch(PRODUCED, 35kg Fleischwurst, MHD 2026-03-20)
    INV->>SM: record(PRODUCTION_OUTPUT, 35kg)
    INV-->>INV: isBelowMinimumLevel()?
    deactivate INV

    Note over QUAL,INV: 4. Qualitätssperre
    QUAL--)INV: QualityHoldCreated Event
    activate INV
    INV->>INV: blockBatch(Fleischwurst-Charge)
    Note over INV: Status: BLOCKED
    deactivate INV

    QUAL--)INV: QualityHoldReleased Event
    activate INV
    INV->>INV: unblockBatch(Fleischwurst-Charge)
    Note over INV: Status: AVAILABLE
    deactivate INV

    Note over INV,SALES: 5. Verkauf (mit FEFO)
    SALES->>INV: reserve(SALE_ORDER, 10kg Fleischwurst, NORMAL)
    activate INV
    INV->>INV: FEFO: älteste Charge zuerst
    INV-->>SALES: ReservationId
    deactivate INV

    SALES->>INV: confirmReservation(reservationId)
    activate INV
    INV->>INV: removeBatch(allocated batches)
    INV->>SM: record(SALE, 10kg)
    deactivate INV
```

### Code-Beispiel

```java
// 1. Wareneingang verarbeiten (Event Handler)
Stock stock = stockRepository
    .findByArticleIdAndStorageLocationId(articleId, locationId)
    .orElseGet(() -> Stock.create(new StockDraft(
        articleId, locationId, "10", "KILOGRAM", 7
    )).value());

var batchDraft = new StockBatchDraft(
    "SUPP-2026-02-19-042",          // Lieferanten-Chargennummer
    "PURCHASED",
    "100", "KILOGRAM",
    "2026-03-15"                     // MHD
);
stock.addBatch(batchDraft);
stockRepository.save(stock);

// StockMovement dokumentieren
StockMovement.record(new StockMovementDraft(
    stock.id().value(), articleId, stockBatchId,
    "SUPP-2026-02-19-042", "PURCHASED",
    "GOODS_RECEIPT", "100", "KILOGRAM",
    null, "GR-2026-02-19-001", userId
));

// 2. Reservierung für Produktionsauftrag
var reserveDraft = new ReservationDraft(
    "PRODUCTION_ORDER", "PO-2026-02-20-001",
    "45", "KILOGRAM", "URGENT"
);
switch (stock.reserve(reserveDraft)) {
    case Result.Success<StockError, Void> s -> {
        stockRepository.save(stock);
        // FEFO hat automatisch die Charge mit MHD 2026-03-15 allokiert
    }
    case Result.Failure<StockError, Void> f -> {
        switch (f.error()) {
            case StockError.InsufficientStock e ->
                log.warn("Nicht genug Material: {}", e.message());
            // ...
        }
    }
}

// 3. Inventur durchführen
var countDraft = new InventoryCountDraft(locationId, "2026-02-19", userId);
InventoryCount count = InventoryCount.create(countDraft).value();

// Soll-Bestände aus Stock laden und als CountItems hinzufügen
for (Stock s : stockRepository.findByStorageLocationId(locationId)) {
    count.addCountItem(new CountItemDraft(
        s.articleId().value(),
        s.getTotalQuantity().amount().toString(),
        s.getTotalQuantity().uom().name()
    ));
}

count.startCounting();

// Zähler erfasst Ist-Menge
count.updateCountItem(countItemId, Quantity.of(new BigDecimal("97"), UnitOfMeasure.KILOGRAM).value());

// Vier-Augen-Prinzip: anderer User schließt ab
count.complete(otherUserId);
inventoryCountRepository.save(count);

// Reconciliation: ADJUSTMENT-Bewegungen für Abweichungen
List<StockMovement> adjustments = reconciliationService.reconcile(count, otherUserId);
adjustments.forEach(stockMovementRepository::save);
```

---

## DDD Validation Checklist

- [x] Aggregate Root ist einziger Einstiegspunkt (Stock, StockMovement, InventoryCount, StorageLocation)
- [x] Alle Änderungen gehen durch Aggregate-Root-Methoden
- [x] Invarianten werden in Factory und Methoden geprüft
- [x] Keine direkten Referenzen auf andere Aggregates (nur IDs: ArticleId, UserId, StorageLocationId, BatchReference)
- [x] Ein Aggregate = eine Transaktionsgrenze
- [x] EntityDraft-Pattern für VO-Konstruktion im Domain Layer
- [x] Result<E,T> für erwartbare Fehler, keine Exceptions
- [x] Sealed interfaces für Domain Errors
- [x] Status-Maschinen explizit dokumentiert (StockBatchStatus, InventoryCountStatus)
- [x] BC-Grenzen klar definiert (QualityHold → Quality BC, Rezeptur → Production BC)
- [x] FEFO-Prinzip im Stock-Aggregate verankert
- [x] Vier-Augen-Prinzip bei Inventur-Abschluss
- [x] Alle Bestandsbewegungen sind chargen-traceable (BatchReference pflicht)
