# Production BC - Detailliertes Domain Model

**Bounded Context:** Production
**Domain Type:** CORE
**Verantwortung:** Rezepturverwaltung, Produktionsplanung, Chargen-Erzeugung mit lückenloser Rückverfolgbarkeit

---

## Ubiquitous Language

| Begriff (DE) | Begriff (EN/Code) | Typ | Definition |
|---|---|---|---|
| Rezept | Recipe | Aggregate | Mehrstufige Anleitung zur Herstellung eines Produkts aus Rohstoffen und Zwischenprodukten |
| Zutat | Ingredient | Entity | Rohstoff oder Zwischenprodukt in einem Rezept. Position bestimmt die Reihenfolge |
| Produktionsschritt | ProductionStep | Entity | Einzelner Arbeitsschritt mit optionaler Dauer und Temperaturangabe |
| Rezepttyp | RecipeType | VO (Enum) | RAW_MATERIAL, INTERMEDIATE oder FINISHED_PRODUCT |
| Ausbeute | YieldPercentage | VO | Input→Output in % (1-200%). Bei 80% braucht man 125kg für 100kg Output |
| Haltbarkeitsdauer | ShelfLifeDays | VO | Tage ab Produktionsdatum bis MHD. Definiert im Rezept |
| Unter-Rezept | SubRecipe | Concept | Verweis auf INTERMEDIATE-Rezept als Zutat (verschachtelte Rezepturen) |
| Rezeptur-Version | Version | VO | Neue Version = neues Recipe-Objekt, altes wird archiviert |
| Charge | Batch | Aggregate | Eindeutig identifizierte Produktionseinheit mit Genealogie |
| Chargennummer | BatchNumber | VO | Formatierte Kennung (Format: "P-YYYY-MM-DD-XXX") |
| Verbrauch | Consumption | Entity | Dokumentation einer verwendeten Input-Charge. Bildet die Genealogie |
| Chargen-Genealogie | Batch Genealogy | Concept | Gesamtheit aller Consumption-Beziehungen. Vorwärts-/Rückwärts-Tracing |
| Vorwärts-Tracing | traceForward | Concept | Rohstoff-Charge → betroffene Endprodukt-Chargen (Rückruf) |
| Rückwärts-Tracing | traceBackward | Concept | Endprodukt-Charge → verwendete Rohstoff-Chargen |
| Ausschuss | Waste | VO | Verlustmenge bei Produktion (Schwund, Knochen, Fett-Trimmen) |
| Produktionsauftrag | ProductionOrder | Aggregate | Geplante Produktion mit Rezept, Menge, Termin, Priorität |
| Priorität | Priority | VO (Enum) | LOW, NORMAL, HIGH, URGENT |
| Freigabe | Release | Concept | PLANNED → RELEASED. Material verfügbar, Produktion darf starten |
| Catch-Weight | Dual Quantity | Concept | Doppelte Mengenführung: Stück + Gewicht (z.B. "10 Stk à 2,3 kg") |
| MHD | BestBeforeDate | VO | Mindesthaltbarkeitsdatum = ProductionDate + ShelfLifeDays |
| Materialbedarf | Material Requirement | Concept | Berechnete Rohstoffmenge aus Rezeptur inkl. Ausbeute |

---

## Aggregate-Übersicht

```mermaid
---
config:
  theme: neutral
  look: classic
  layout: elk
  themeVariables:
    background: "#f8fafc"
  class:
    hideEmptyMembersBox: true
---
classDiagram
    class Recipe {
        +RecipeId id
        +ArticleId articleId
        +String name
        +int version
        +RecipeType recipeType
        +YieldPercentage yieldPercentage
        +int shelfLifeDays
        +RecipeStatus status
        +List~Ingredient~ ingredients
        +List~ProductionStep~ productionSteps
        +create(RecipeDraft) Result~RecipeError, Recipe~
        +addIngredient(IngredientDraft) Result~RecipeError, Void~
        +removeIngredient(IngredientId) Result~RecipeError, Void~
        +addProductionStep(ProductionStepDraft) Result~RecipeError, Void~
        +activate() Result~RecipeError, Void~
        +archive() Result~RecipeError, Void~
        +calculateRequiredInput(Quantity) Quantity
        +calculateBestBeforeDate(LocalDate) LocalDate
    }

    class Ingredient {
        +IngredientId id
        +int position
        +ArticleId articleId
        +Quantity quantity
        +RecipeId subRecipeId
        +boolean substitutable
    }

    class ProductionStep {
        +int stepNumber
        +String description
        +Integer durationMinutes
        +Integer temperatureCelsius
    }

    class Batch {
        +BatchId id
        +BatchNumber batchNumber
        +RecipeId recipeId
        +ArticleId articleId
        +Quantity plannedQuantity
        +Quantity actualQuantity
        +Quantity waste
        +LocalDate productionDate
        +LocalDate bestBeforeDate
        +BatchStatus status
        +UserId producedBy
        +BranchId branchId
        +List~Consumption~ consumptions
        +plan(BatchDraft) Result~BatchError, Batch~
        +startProduction() Result~BatchError, Void~
        +recordConsumption(ConsumptionDraft) Result~BatchError, Void~
        +complete(Quantity, Quantity, String) Result~BatchError, Void~
        +cancel(String) Result~BatchError, Void~
        +isFullyTraceable() boolean
        +getUpstreamBatchIds() List~BatchId~
    }

    class Consumption {
        +ConsumptionId id
        +BatchId inputBatchId
        +ArticleId articleId
        +Quantity quantityUsed
        +Instant consumedAt
    }

    class ProductionOrder {
        +ProductionOrderId id
        +RecipeId recipeId
        +ArticleId articleId
        +Quantity plannedQuantity
        +LocalDate plannedDate
        +Priority priority
        +ProductionOrderStatus status
        +UserId createdBy
        +Instant createdAt
        +BranchId branchId
        +BatchId batchId
        +create(ProductionOrderDraft) Result~ProductionOrderError, ProductionOrder~
        +release() Result~ProductionOrderError, Void~
        +startProduction(BatchId) Result~ProductionOrderError, Void~
        +complete() Result~ProductionOrderError, Void~
        +cancel(String) Result~ProductionOrderError, Void~
        +reschedule(LocalDate) Result~ProductionOrderError, Void~
    }

    Recipe "1" *-- "*" Ingredient : enthält
    Recipe "1" *-- "*" ProductionStep : enthält
    Batch "1" *-- "*" Consumption : enthält
    ProductionOrder "1" --> "0..1" Batch : erzeugt

    Recipe ..> RecipeId
    Recipe ..> ArticleId
    Recipe ..> YieldPercentage
    Recipe ..> RecipeType
    Recipe ..> RecipeStatus

    Batch ..> BatchId
    Batch ..> BatchNumber
    Batch ..> BatchStatus
    Batch ..> Quantity

    ProductionOrder ..> ProductionOrderId
    ProductionOrder ..> Priority
    ProductionOrder ..> ProductionOrderStatus
```

---

## Aggregates

### 1. Recipe (Aggregate Root)

**Verantwortung:** Verwaltet mehrstufige Rezepturen mit Zutaten, Produktionsschritten und Ausbeute-Berechnung.

```
Recipe (Aggregate Root)
├── RecipeId (VO)
├── ArticleId (VO) - Referenz auf Stammdaten-Artikel (Endprodukt)
├── Name (VO)
├── Version (VO: int) - Versionierung bei Rezepturänderungen
├── RecipeType (VO: RAW_MATERIAL | INTERMEDIATE | FINISHED_PRODUCT)
├── YieldPercentage (VO) - Ausbeute in % (1-200)
├── ShelfLifeDays (VO: int) - Haltbarkeitsdauer → MHD-Berechnung
├── Ingredients[] (Entity)
│   ├── IngredientId (VO)
│   ├── Position (VO: int) - Reihenfolge im Rezept
│   ├── ArticleId (VO) - Referenz auf Stammdaten-Artikel (Zutat)
│   ├── Quantity (VO) - Menge mit UnitOfMeasure + optionalem Catch-Weight
│   ├── SubRecipeId (VO) - Für Zwischenprodukte (verschachtelte Rezepte)
│   └── Substitutable (boolean) - Kann durch Alternativzutat ersetzt werden?
├── ProductionSteps[] (Entity)
│   ├── StepNumber (VO: int) - Sequentiell
│   ├── Description (VO: String)
│   ├── DurationMinutes (VO: Integer) - Optional, geschätzte Dauer
│   └── TemperatureCelsius (VO: Integer) - Optional, für Räuchern/Kochen
└── Status (VO: DRAFT | ACTIVE | ARCHIVED)
```

**Invarianten:**
```java
/**
 * Recipe aggregate root.
 *
 * Invariants:
 * - ACTIVE recipe must have at least one ingredient
 * - YieldPercentage must be between 1-200% (>100% bei Pökel-/Spritzprozessen)
 * - Ingredient quantities must be positive
 * - Nested recipes (SubRecipeId) cannot create circular dependencies
 * - Position numbers in Ingredients must be unique and sequential
 * - StepNumber in ProductionSteps must be sequential (1, 2, 3, ...)
 * - ShelfLifeDays must be > 0 for FINISHED_PRODUCT and INTERMEDIATE
 * - Only DRAFT recipes can be modified (add/remove ingredients, steps)
 * - Version is immutable after creation; new version = new Recipe
 * - Status transitions: DRAFT → ACTIVE → ARCHIVED (no way back)
 */
```

**Draft-Records:**
```java
public record RecipeDraft(
    String articleId,
    String name,
    String recipeType,       // RAW_MATERIAL | INTERMEDIATE | FINISHED_PRODUCT
    Integer yieldPercentage,
    Integer shelfLifeDays
) {}

public record IngredientDraft(
    int position,
    String articleId,
    String quantityAmount,   // BigDecimal als String
    String quantityUnit,     // UnitOfMeasure
    String subRecipeId,      // nullable
    boolean substitutable
) {}

public record ProductionStepDraft(
    int stepNumber,
    String description,
    Integer durationMinutes,     // nullable
    Integer temperatureCelsius   // nullable
) {}
```

**Factory & Business Methods:**
```java
// Factory
public static Result<RecipeError, Recipe> create(RecipeDraft draft);

// Mutations (nur im DRAFT-Status)
public Result<RecipeError, Void> addIngredient(IngredientDraft draft);
public Result<RecipeError, Void> removeIngredient(IngredientId ingredientId);
public Result<RecipeError, Void> addProductionStep(ProductionStepDraft draft);
public Result<RecipeError, Void> removeProductionStep(int stepNumber);

// Status-Übergänge
public Result<RecipeError, Void> activate();
public Result<RecipeError, Void> archive();

// Query Methods
public Quantity calculateRequiredInput(Quantity desiredOutput);
public LocalDate calculateBestBeforeDate(LocalDate productionDate);
public List<ArticleId> getAllIngredientArticleIds();
public boolean containsSubRecipe();
```

**Domain Events:**
```java
RecipeCreated(RecipeId, ArticleId, RecipeType)
RecipeActivated(RecipeId)
RecipeArchived(RecipeId)
```

---

### 2. Batch (Aggregate Root)

**Verantwortung:** Repräsentiert eine Produktions-Charge mit lückenloser Rückverfolgbarkeit (Chargen-Genealogie). Zentral für HACCP-Compliance und Rückruf-Szenarien.

```
Batch (Aggregate Root)
├── BatchId (VO)
├── BatchNumber (VO) - Formatierte Chargennummer (z.B. "P-2026-02-17-001")
├── RecipeId (VO) - Referenz auf verwendete Rezeptur
├── ArticleId (VO) - Referenz auf produziertes Endprodukt
├── PlannedQuantity (VO: Quantity) - Soll-Menge
├── ActualQuantity (VO: Quantity) - Ist-Menge nach Rückmeldung
├── Waste (VO: Quantity) - Ausschuss/Schwund
├── ProductionDate (VO: LocalDate)
├── BestBeforeDate (VO: LocalDate) - MHD, berechnet aus Recipe.ShelfLifeDays
├── Status (VO: PLANNED | IN_PRODUCTION | COMPLETED | CANCELLED)
├── ProducedBy (VO: UserId)
├── BranchId (VO) - Produktionsstandort
├── Consumptions[] (Entity) - Chargen-Genealogie (Input-Tracking)
│   ├── ConsumptionId (VO)
│   ├── InputBatchId (VO) - Verbrauchte Rohstoff-/Zwischen-Charge
│   ├── ArticleId (VO) - Zutat-Artikel
│   ├── QuantityUsed (VO: Quantity) - Verbrauchte Menge (mit Catch-Weight)
│   └── ConsumedAt (VO: Instant)
├── Remarks (VO: String) - Optional, für Abweichungen
├── CancelledReason (VO: String) - Optional, Grund bei Stornierung
├── StartedAt (VO: Instant)
└── CompletedAt (VO: Instant)
```

**Invarianten:**
```java
/**
 * Batch aggregate root (ProductionBatch).
 *
 * Invariants:
 * - PlannedQuantity must be positive
 * - BestBeforeDate must be after ProductionDate
 * - Status transitions: PLANNED → IN_PRODUCTION → COMPLETED
 *                       PLANNED → CANCELLED
 *                       IN_PRODUCTION → CANCELLED
 * - Cannot complete without at least one Consumption recorded
 * - ActualQuantity must be positive at completion
 * - Waste must be >= 0
 * - All Consumptions must reference distinct InputBatchIds
 * - Consumptions can only be added in IN_PRODUCTION status
 * - CompletedAt must be after StartedAt
 * - BatchNumber is immutable after creation
 */
```

**Draft-Record:**
```java
public record BatchDraft(
    String recipeId,
    String articleId,
    String plannedQuantityAmount,   // BigDecimal als String
    String plannedQuantityUnit,     // UnitOfMeasure
    String productionDate,          // ISO LocalDate
    String bestBeforeDate,          // ISO LocalDate
    String producedBy,              // UserId
    String branchId
) {}

public record ConsumptionDraft(
    String inputBatchId,
    String articleId,
    String quantityAmount,          // BigDecimal als String
    String quantityUnit             // UnitOfMeasure
) {}
```

**Factory & Business Methods:**
```java
// Factory
public static Result<BatchError, Batch> plan(BatchDraft draft);

// Status-Übergänge
public Result<BatchError, Void> startProduction();
public Result<BatchError, Void> complete(Quantity actualQuantity, Quantity waste, String remarks);
public Result<BatchError, Void> cancel(String reason);

// Genealogie (nur in IN_PRODUCTION)
public Result<BatchError, Void> recordConsumption(ConsumptionDraft draft);

// Query Methods
public boolean isFullyTraceable();
public List<BatchId> getUpstreamBatchIds();
public Duration getProductionDuration();
```

**Domain Events:**
```java
BatchPlanned(BatchId, BatchNumber, RecipeId, ArticleId, Quantity)
BatchStarted(BatchId)
BatchCompleted(BatchId, ArticleId, Quantity, LocalDate bestBeforeDate, BranchId)
  → triggers Inventory stock-in
BatchCancelled(BatchId, String reason)
ConsumptionRecorded(BatchId, BatchId inputBatchId, Quantity)
  → triggers Inventory stock-out for consumed materials
```

---

### 3. ProductionOrder (Aggregate Root)

**Verantwortung:** Plant zukünftige Produktionen. Verbindet Produktionsplanung mit der konkreten Chargen-Erzeugung.

```
ProductionOrder (Aggregate Root)
├── ProductionOrderId (VO)
├── RecipeId (VO)
├── ArticleId (VO) - Zu produzierender Artikel
├── PlannedQuantity (VO: Quantity)
├── PlannedDate (VO: LocalDate)
├── Priority (VO: LOW | NORMAL | HIGH | URGENT)
├── Status (VO: PLANNED | RELEASED | IN_PRODUCTION | COMPLETED | CANCELLED)
├── CreatedBy (VO: UserId)
├── CreatedAt (VO: Instant)
├── BranchId (VO) - Ziel-Produktionsstandort
├── BatchId (VO) - Verknüpfung zur erzeugten Charge (nach Start)
├── Remarks (VO: String) - Optional
└── CancelledReason (VO: String) - Optional
```

**Invarianten:**
```java
/**
 * ProductionOrder aggregate root.
 *
 * Invariants:
 * - PlannedQuantity must be positive
 * - PlannedDate cannot be in the past at creation
 * - Status transitions: PLANNED → RELEASED → IN_PRODUCTION → COMPLETED
 *                       PLANNED → CANCELLED
 *                       RELEASED → CANCELLED
 * - Can only release if RecipeId references an ACTIVE recipe (checked in Application Layer)
 * - Cannot start production without setting BatchId
 * - Cannot complete without linked Batch being COMPLETED (checked in Application Layer)
 * - BatchId is set exactly once (at startProduction)
 * - Reschedule only allowed in PLANNED or RELEASED status
 */
```

**Draft-Record:**
```java
public record ProductionOrderDraft(
    String recipeId,
    String articleId,
    String plannedQuantityAmount,   // BigDecimal als String
    String plannedQuantityUnit,     // UnitOfMeasure
    String plannedDate,             // ISO LocalDate
    String priority,                // LOW | NORMAL | HIGH | URGENT
    String branchId,
    String createdBy,
    String remarks                  // nullable
) {}
```

**Factory & Business Methods:**
```java
// Factory
public static Result<ProductionOrderError, ProductionOrder> create(ProductionOrderDraft draft);

// Status-Übergänge
public Result<ProductionOrderError, Void> release();
public Result<ProductionOrderError, Void> startProduction(BatchId batchId);
public Result<ProductionOrderError, Void> complete();
public Result<ProductionOrderError, Void> cancel(String reason);

// Mutations
public Result<ProductionOrderError, Void> reschedule(LocalDate newDate);
```

**Domain Events:**
```java
ProductionOrderCreated(ProductionOrderId, RecipeId, Quantity, LocalDate)
ProductionOrderReleased(ProductionOrderId)
  → triggers material availability check / Procurement demand
ProductionOrderStarted(ProductionOrderId, BatchId)
ProductionOrderCompleted(ProductionOrderId, BatchId)
ProductionOrderCancelled(ProductionOrderId, String reason)
ProductionOrderRescheduled(ProductionOrderId, LocalDate oldDate, LocalDate newDate)
```

---

## Shared Value Objects

```mermaid
---
config:
  theme: neutral
  look: classic
  layout: elk
  themeVariables:
    background: "#f8fafc"
  class:
    hideEmptyMembersBox: true
---
classDiagram
    class Quantity {
        +BigDecimal amount
        +UnitOfMeasure uom
        +BigDecimal secondaryAmount
        +UnitOfMeasure secondaryUom
        +of(BigDecimal, UnitOfMeasure) Result
        +dual(BigDecimal, UnitOfMeasure, BigDecimal, UnitOfMeasure) Result
        +hasDualQuantity() boolean
        +add(Quantity) Quantity
        +subtract(Quantity) Quantity
        +multiply(BigDecimal) Quantity
        +isZero() boolean
        +isPositive() boolean
    }

    class UnitOfMeasure {
        <<enumeration>>
        KILOGRAM
        GRAM
        LITER
        MILLILITER
        PIECE
        METER
    }

    class YieldPercentage {
        +int value
        +calculateRequiredInput(Quantity) Quantity
    }

    class BatchNumber {
        +String value
        +generate(LocalDate, int) BatchNumber
    }

    class RecipeType {
        <<enumeration>>
        RAW_MATERIAL
        INTERMEDIATE
        FINISHED_PRODUCT
    }

    class Priority {
        <<enumeration>>
        LOW
        NORMAL
        HIGH
        URGENT
    }

    class RecipeStatus {
        <<enumeration>>
        DRAFT
        ACTIVE
        ARCHIVED
    }

    class BatchStatus {
        <<enumeration>>
        PLANNED
        IN_PRODUCTION
        COMPLETED
        CANCELLED
    }

    class ProductionOrderStatus {
        <<enumeration>>
        PLANNED
        RELEASED
        IN_PRODUCTION
        COMPLETED
        CANCELLED
    }

    Quantity --> UnitOfMeasure
```

### Quantity (mit Catch-Weight / Dual-Quantity)

Zentral für die gesamte Produktion. Unterstützt Dual-Quantity für Catch-Weight-Artikel (z.B. "10 Stück à 2,3 kg").

```java
public record Quantity(
    BigDecimal amount,
    UnitOfMeasure uom,
    BigDecimal secondaryAmount,     // Catch-Weight, nullable
    UnitOfMeasure secondaryUom      // nullable
) {
    // Factory: einfache Menge
    public static Result<QuantityError, Quantity> of(BigDecimal amount, UnitOfMeasure uom);

    // Factory: Dual-Quantity (Catch-Weight)
    public static Result<QuantityError, Quantity> dual(
        BigDecimal primaryAmount, UnitOfMeasure primaryUom,
        BigDecimal secondaryAmount, UnitOfMeasure secondaryUom
    );

    public boolean hasDualQuantity();
    public Quantity add(Quantity other);
    public Quantity subtract(Quantity other);
    public Quantity multiply(BigDecimal factor);
    public boolean isZero();
    public boolean isPositive();
}
```

### UnitOfMeasure

```java
public enum UnitOfMeasure {
    KILOGRAM("kg"),
    GRAM("g"),
    LITER("l"),
    MILLILITER("ml"),
    PIECE("Stk"),
    METER("m");

    private final String abbreviation;
}
```

### BatchNumber

```java
public record BatchNumber(String value) {
    // Format: "P-YYYY-MM-DD-XXX" (P = Production)
    public static BatchNumber generate(LocalDate productionDate, int sequenceNumber) {
        String value = String.format("P-%s-%03d", productionDate, sequenceNumber);
        return new BatchNumber(value);
    }
}
```

### YieldPercentage

```java
public record YieldPercentage(int value) {
    public YieldPercentage {
        if (value < 1 || value > 200) {
            throw new IllegalArgumentException(
                "Yield percentage must be between 1-200, got: " + value
            );
        }
    }

    /**
     * Berechnet benötigten Input für gewünschten Output.
     * Bei 80% Ausbeute und 100kg gewünschtem Output → 125kg Input nötig.
     */
    public Quantity calculateRequiredInput(Quantity desiredOutput) {
        return desiredOutput.multiply(new BigDecimal(100).divide(
            new BigDecimal(value), 4, RoundingMode.HALF_UP));
    }
}
```

### Priority

```java
public enum Priority {
    LOW,
    NORMAL,
    HIGH,
    URGENT
}
```

---

## Domain Services

### RecipeCycleDependencyChecker

```java
/**
 * Prüft ob verschachtelte Rezepturen zirkuläre Abhängigkeiten erzeugen.
 * Wird im Application Layer aufgerufen nach Recipe.addIngredient() mit SubRecipeId.
 */
public class RecipeCycleDependencyChecker {
    public Result<RecipeError, Void> check(RecipeId recipeId, RecipeRepository repository);
}
```

### BatchTraceabilityService

```java
/**
 * Rückverfolgbarkeits-Service für HACCP-Compliance und Rückruf-Szenarien.
 * Traversiert Chargen-Genealogie über BatchConsumptions.
 */
public class BatchTraceabilityService {
    /**
     * Vorwärts-Tracing: Welche Endprodukt-Chargen verwenden diese Rohstoff-Charge?
     * KRITISCH für Rückruf-Szenarien.
     */
    public List<BatchId> traceForward(BatchId sourceBatchId);

    /**
     * Rückwärts-Tracing: Welche Rohstoff-Chargen stecken in diesem Endprodukt?
     */
    public List<BatchId> traceBackward(BatchId targetBatchId);
}
```

**Chargen-Genealogie (Beispiel):**

```mermaid
---
config:
  theme: neutral
  look: classic
  layout: elk
  themeVariables:
    background: "#f8fafc"
---
graph TB
    subgraph ROHSTOFFE["Rohstoff-Chargen (Wareneingang)"]
        RB1["P-2026-02-10-001\nSchweineschulter\n50kg, MHD 2026-03-01"]
        RB2["P-2026-02-11-003\nGewürzmischung A\n5kg, MHD 2026-08-01"]
        RB3["P-2026-02-10-007\nNitritpökelsalz\n2kg, MHD 2027-01-01"]
    end

    subgraph ZWISCHEN["Zwischen-Charge"]
        ZB1["P-2026-02-15-001\nBrät (Intermediate)\n40kg, MHD 2026-02-18"]
    end

    subgraph ENDPRODUKT["Endprodukt-Chargen"]
        EB1["P-2026-02-15-002\nFleischwurst\n35kg, MHD 2026-03-15"]
    end

    RB1 -->|"Consumption\n45kg"| ZB1
    RB2 -->|"Consumption\n3kg"| ZB1
    RB3 -->|"Consumption\n1.5kg"| ZB1
    ZB1 -->|"Consumption\n38kg"| EB1

    style RB1 fill:#e3f2fd
    style RB2 fill:#e3f2fd
    style RB3 fill:#e3f2fd
    style ZB1 fill:#fff3e0
    style EB1 fill:#e8f5e9

    EB1 -. "Rückwärts-Tracing\n(traceBackward)" .-> ZB1
    ZB1 -. "Rückwärts-Tracing" .-> RB1
    RB1 -. "Vorwärts-Tracing\n(traceForward)" .-> ZB1
    ZB1 -. "Vorwärts-Tracing" .-> EB1
```

### BatchNumberGenerator

```java
/**
 * Generiert eindeutige Chargennummern. Infrastructure-Concern (Sequenz aus DB).
 */
public interface BatchNumberGenerator {
    BatchNumber generate(LocalDate productionDate);
}
```

---

## Domain Errors

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

    record RecipeNotFound(String recipeId) implements RecipeError {
        public String message() { return "Recipe not found: " + recipeId; }
    }
    record InvalidYieldPercentage(int value) implements RecipeError {
        public String message() { return "Yield must be 1-200%, got: " + value; }
    }
    record CyclicDependencyDetected(String path) implements RecipeError {
        public String message() { return "Cyclic recipe dependency: " + path; }
    }
    record NoIngredients() implements RecipeError {
        public String message() { return "Recipe must have at least one ingredient"; }
    }
    record DuplicatePosition(int position) implements RecipeError {
        public String message() { return "Duplicate ingredient position: " + position; }
    }
    record NotInDraftStatus() implements RecipeError {
        public String message() { return "Recipe can only be modified in DRAFT status"; }
    }
    record InvalidShelfLife(int days) implements RecipeError {
        public String message() { return "Shelf life must be > 0, got: " + days; }
    }
    record InvalidQuantity(String reason) implements RecipeError {
        public String message() { return "Invalid quantity: " + reason; }
    }
}

public sealed interface BatchError {
    String message();

    record InvalidQuantity(String reason) implements BatchError {
        public String message() { return "Invalid quantity: " + reason; }
    }
    record InvalidStatusTransition(String from, String to) implements BatchError {
        public String message() { return "Cannot transition from " + from + " to " + to; }
    }
    record MissingConsumptions() implements BatchError {
        public String message() { return "Cannot complete batch without recorded consumptions"; }
    }
    record InvalidBestBeforeDate() implements BatchError {
        public String message() { return "Best-before date must be after production date"; }
    }
    record DuplicateInputBatch(String batchId) implements BatchError {
        public String message() { return "Input batch already recorded: " + batchId; }
    }
    record NegativeWaste() implements BatchError {
        public String message() { return "Waste cannot be negative"; }
    }
}

public sealed interface ProductionOrderError {
    String message();

    record PlannedDateInPast(String date) implements ProductionOrderError {
        public String message() { return "Planned date is in the past: " + date; }
    }
    record InvalidQuantity(String reason) implements ProductionOrderError {
        public String message() { return "Invalid quantity: " + reason; }
    }
    record InvalidStatusTransition(String from, String to) implements ProductionOrderError {
        public String message() { return "Cannot transition from " + from + " to " + to; }
    }
    record BatchAlreadyAssigned(String batchId) implements ProductionOrderError {
        public String message() { return "Batch already assigned: " + batchId; }
    }
}
```

---

## Repository Interfaces

```java
public interface RecipeRepository {
    Optional<Recipe> findById(RecipeId id);
    List<Recipe> findByStatus(RecipeStatus status);
    List<Recipe> findByArticleId(ArticleId articleId);
    List<Recipe> findActiveByIngredientArticleId(ArticleId ingredientArticleId);
    void save(Recipe recipe);
    boolean existsByNameAndVersion(String name, int version);
}

public interface BatchRepository {
    Optional<Batch> findById(BatchId id);
    Optional<Batch> findByBatchNumber(BatchNumber batchNumber);
    List<Batch> findByProductionDate(LocalDate date);
    List<Batch> findByStatus(BatchStatus status);
    List<Batch> findByArticleId(ArticleId articleId);
    // Für Rückverfolgbarkeit
    List<Batch> findByInputBatchId(BatchId upstreamBatchId);
    void save(Batch batch);
}

public interface ProductionOrderRepository {
    Optional<ProductionOrder> findById(ProductionOrderId id);
    List<ProductionOrder> findByPlannedDate(LocalDate date);
    List<ProductionOrder> findByPlannedDateRange(LocalDate from, LocalDate to);
    List<ProductionOrder> findByStatus(ProductionOrderStatus status);
    void save(ProductionOrder order);
}
```

---

## Status-Maschinen

### Recipe Status

```mermaid
---
config:
  theme: neutral
  themeVariables:
    background: "#f8fafc"
---
stateDiagram-v2
    [*] --> DRAFT : create()
    DRAFT --> ACTIVE : activate()
    ACTIVE --> ARCHIVED : archive()

    DRAFT : Editierbar
    DRAFT : addIngredient(), removeIngredient()
    DRAFT : addProductionStep(), removeProductionStep()
    ACTIVE : Nur lesen, in Produktion verwendbar
    ARCHIVED : Nur lesen, nicht mehr verwendbar
```

### Batch Status

```mermaid
---
config:
  theme: neutral
  themeVariables:
    background: "#f8fafc"
---
stateDiagram-v2
    [*] --> PLANNED : plan()
    PLANNED --> IN_PRODUCTION : startProduction()
    PLANNED --> CANCELLED : cancel()
    IN_PRODUCTION --> COMPLETED : complete()
    IN_PRODUCTION --> CANCELLED : cancel()

    PLANNED : Charge geplant
    IN_PRODUCTION : recordConsumption()
    IN_PRODUCTION : Genealogie dokumentieren
    COMPLETED : actualQuantity + waste gesetzt
    COMPLETED : → BatchCompleted Event
    CANCELLED : cancelledReason gesetzt
```

### ProductionOrder Status

```mermaid
---
config:
  theme: neutral
  themeVariables:
    background: "#f8fafc"
---
stateDiagram-v2
    [*] --> PLANNED : create()
    PLANNED --> RELEASED : release()
    PLANNED --> CANCELLED : cancel()
    RELEASED --> IN_PRODUCTION : startProduction(batchId)
    RELEASED --> CANCELLED : cancel()
    IN_PRODUCTION --> COMPLETED : complete()

    PLANNED : reschedule() möglich
    RELEASED : Material-Check bestanden
    RELEASED : reschedule() möglich
    RELEASED : → ProductionOrderReleased Event
    IN_PRODUCTION : BatchId zugewiesen
    COMPLETED : Batch ist COMPLETED
```

---

## 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)"]
        FI["Filiales\n(BranchId)"]
    end

    subgraph PRODUCTION["Production BC"]
        R["Recipe"]
        B["Batch"]
        PO["ProductionOrder"]
    end

    subgraph DOWNSTREAM["Downstream BCs"]
        INV["Inventory BC"]
        PROC["Procurement BC"]
        LAB["Labeling BC"]
    end

    MD -->|ArticleId| R
    MD -->|ArticleId| B
    MD -->|ArticleId| PO
    UM -->|UserId| B
    UM -->|UserId| PO
    FI -->|BranchId| B
    FI -->|BranchId| PO

    PO -->|erzeugt| B
    R -->|RecipeId| B
    R -->|RecipeId| PO

    B -->|"BatchCompleted\n(stock-in)"| INV
    B -->|"ConsumptionRecorded\n(stock-out)"| INV
    PO -->|"OrderReleased\n(demand)"| PROC
    R -->|"RecipeActivated\n(Nährwerte)"| LAB
```

### Upstream-Abhängigkeiten (Production konsumiert)
| BC | Referenz | Zweck |
|---|---|---|
| **Master Data** | ArticleId | Artikel-Referenz in Recipe, Batch, ProductionOrder |
| **User Management** | UserId | ProducedBy in Batch, CreatedBy in ProductionOrder |
| **Filiales** | BranchId | Produktionsstandort |

### Downstream-Integrationen (Production publiziert Events)
| Event | Konsument | Aktion |
|---|---|---|
| `BatchCompleted` | **Inventory BC** | Stock-In für produzierte Ware |
| `ConsumptionRecorded` | **Inventory BC** | Stock-Out für verbrauchte Rohstoffe |
| `ProductionOrderReleased` | **Procurement BC** | Materialbedarf / Demand Planning |
| `RecipeActivated` | **Labeling BC** | Nährwert-/Allergen-Berechnung |

### Abgrenzungen (gehören NICHT in Production BC)
| Konzept | Zuständiger BC | Grund |
|---|---|---|
| QualityHold / Freigabe | **Quality BC** | Eigene Aggregate-Logik mit QualityStatus |
| StockReservation | **Inventory BC** | Bestandsführung ist Inventory-Concern |
| Labeling / Deklaration | **Labeling BC** | Nährwert- und Allergen-Deklaration |
| Lieferanten-Chargen | **Master Data BC** | SupplierBatchNumber ist Stammdaten-Concern |

---

## Use Cases (Application Layer)

```java
// Recipe Management
CreateRecipe        → Recipe.create(RecipeDraft)
AddRecipeIngredient → recipe.addIngredient(IngredientDraft)
ActivateRecipe      → recipe.activate()
ArchiveRecipe       → recipe.archive()
GetRecipe           → Query
ListActiveRecipes   → Query

// Batch Management
PlanBatch           → Batch.plan(BatchDraft)
StartBatch          → batch.startProduction()
RecordConsumption   → batch.recordConsumption(ConsumptionDraft)
CompleteBatch       → batch.complete(actualQty, waste, remarks)
CancelBatch         → batch.cancel(reason)
TraceBatchForward   → BatchTraceabilityService.traceForward(batchId)
TraceBatchBackward  → BatchTraceabilityService.traceBackward(batchId)

// ProductionOrder Management
CreateProductionOrder → ProductionOrder.create(ProductionOrderDraft)
ReleaseOrder          → order.release()
StartProductionOrder  → order.startProduction(batchId) + batch.startProduction()
CompleteOrder         → order.complete()
CancelOrder           → order.cancel(reason)
RescheduleOrder       → order.reschedule(newDate)
ListOrdersByDate      → Query
```

---

## Beispiel: Produktionsfluss (End-to-End)
TODO: Was passiert wenn Material da/nicht da? Woher ist klar dass Produktion gestartet werden kann?
```mermaid
---
config:
  theme: neutral
  themeVariables:
    background: "#f8fafc"
---
sequenceDiagram
    participant M as Metzgermeister
    participant PO as ProductionOrder
    participant R as Recipe
    participant B as Batch
    participant INV as Inventory BC
    participant PROC as Procurement BC

    M->>PO: create(ProductionOrderDraft)
    activate PO
    PO-->>M: ProductionOrder (PLANNED)

    M->>PO: release()
    PO--)PROC: ProductionOrderReleased Event
    Note over PROC: Materialbedarf prüfen

    M->>PO: startProduction(batchId)
    PO->>B: plan(BatchDraft)
    activate B
    B-->>PO: Batch (PLANNED)
    B->>B: startProduction()
    Note over B: Status: IN_PRODUCTION

    M->>B: recordConsumption(Schweineschulter, 125kg)
    B--)INV: ConsumptionRecorded Event
    Note over INV: Stock-Out: 125kg Schweineschulter

    M->>B: recordConsumption(Gewürz, 3kg)
    B--)INV: ConsumptionRecorded Event

    M->>B: complete(80kg, 5kg waste)
    B--)INV: BatchCompleted Event
    Note over INV: Stock-In: 80kg Endprodukt
    deactivate B

    M->>PO: complete()
    deactivate PO
    Note over PO: Status: COMPLETED
```

### Code-Beispiel

```java
// 1. Rezeptur existiert (ACTIVE)
Recipe recipe = recipeRepository.findById(recipeId).orElseThrow();

// 2. Produktionsauftrag erstellen
var orderDraft = new ProductionOrderDraft(
    recipeId, articleId, "100", "KILOGRAM",
    "2026-02-20", "NORMAL", branchId, userId, null
);
ProductionOrder order = ProductionOrder.create(orderDraft).value();

// 3. Auftrag freigeben (Material-Check im Application Layer)
order.release();

// 4. Produktion starten → Charge erzeugen
var batchDraft = new BatchDraft(
    recipeId, articleId, "100", "KILOGRAM",
    "2026-02-20", "2026-03-22",  // MHD = ProductionDate + ShelfLifeDays
    userId, branchId
);
Batch batch = Batch.plan(batchDraft).value();
batch.startProduction();
order.startProduction(batch.id());

// 5. Rohstoff-Verbrauch dokumentieren (Genealogie)
batch.recordConsumption(new ConsumptionDraft(
    "P-2026-02-15-042",  // Rohstoff-Charge
    "ART-001",           // Schweineschulter
    "125", "KILOGRAM"   // 125kg Input für 100kg Output bei 80% Ausbeute
));

// 6. Charge abschließen
batch.complete(
    Quantity.of(new BigDecimal("80"), UnitOfMeasure.KILOGRAM).value(),
    Quantity.of(new BigDecimal("5"), UnitOfMeasure.KILOGRAM).value(),
    "Leichter Schwund beim Räuchern"
);
order.complete();

// 7. Events:
//    BatchCompleted → Inventory bucht 80kg Endprodukt ein
//    ConsumptionRecorded → Inventory bucht 125kg Rohstoff aus
```

---

## DDD Validation Checklist

- [x] Aggregate Root ist einziger Einstiegspunkt (Recipe, Batch, ProductionOrder)
- [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, BranchId, BatchId)
- [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
- [x] BC-Grenzen klar definiert (QualityHold → Quality BC, Stock → Inventory BC)
