# Produktions-Kontext - Detailliertes Domain Model

**Bounded Context:** Produktion
**Domain-Typ:** KERN
**Verantwortung:** Rezepturverwaltung, Produktionsplanung, Chargen-Erzeugung

---

## Aggregate

### 1. Rezept (Aggregate Root)

**Verantwortung:** Verwaltet mehrstufige Rezepturen mit Zutaten und Produktionsschritten.

**Struktur:**
```
Rezept (Aggregate Root)
├── RezeptId (Wertobjekt)
├── Name (Wertobjekt)
├── Version (Wertobjekt)
├── RezeptTyp (Wertobjekt: ROHSTOFF | ZWISCHENPRODUKT | ENDPRODUKT)
├── AusbeuteInProzent (Wertobjekt) - Ausbeute in %
├── Zutaten[] (Entität)
│   ├── Position (Wertobjekt) - Reihenfolge im Rezept
│   ├── ArtikelId (Wertobjekt) - Referenz zu Stammdaten
│   ├── Menge (Wertobjekt)
│   ├── RezeptId (Wertobjekt) - Für Zwischenprodukte (verschachtelte Rezepte)
│   └── IstErsetzbar (Wertobjekt) - Kann durch Alternativzutat ersetzt werden?
├── Produktionsschritte[] (Entität)
│   ├── SchrittNummer (Wertobjekt)
│   ├── Beschreibung (Wertobjekt)
│   ├── Dauer (Wertobjekt) - Geschätzte Dauer
│   └── Temperatur (Wertobjekt) - Optional, für Räuchern/Kochen
└── Status (Wertobjekt: ENTWURF | AKTIV | ARCHIVIERT)
```

**Invarianten:**
```java
/**
 * Rezept Aggregate Root.
 *
 * Invarianten:
 * - Rezept muss mindestens eine Zutat haben
 * - Ausbeute muss zwischen 1-100% liegen
 * - Zutatmengen müssen positiv sein
 * - Verschachtelte Rezepte dürfen keine Zyklen erzeugen (A → B → A)
 * - Summe der Zutatmengen sollte ungefähr dem erwarteten Input entsprechen
 * - Positionsnummern bei Zutaten müssen eindeutig sein
 * - SchrittNummer bei Produktionsschritten muss sequenziell sein (1, 2, 3, ...)
 */
```

**Geschäftsmethoden:**
```java
// Zutat hinzufügen
public Result<RezeptFehler, Void> zutatHinzufuegen(
    ArtikelId artikelId,
    Menge menge,
    Optional<RezeptId> verschachteltesRezeptId
);

// Zutat entfernen
public Result<RezeptFehler, Void> zutatEntfernen(ArtikelId artikelId);

// Ausbeute aktualisieren
public Result<RezeptFehler, Void> ausbeuteAktualisieren(AusbeuteInProzent neueAusbeute);

// Produktionsschritt hinzufügen
public Result<RezeptFehler, Void> produktionsschrittHinzufuegen(
    String beschreibung,
    Optional<Duration> dauer
);

// Rezept aktivieren
public Result<RezeptFehler, Void> aktivieren();

// Rezept archivieren
public Result<RezeptFehler, Void> archivieren();

// Abfragemethoden
public Menge berechneBenoeteigtenInput(Menge gewuenschterOutput);
public List<ArtikelId> holeAlleZutaten(); // Flach, inkl. verschachtelte
public boolean enthaeltVerschachtelteRezepte();
```

**Domänen-Events:**
```java
RezeptErstellt
RezeptAktiviert
RezeptArchiviert
ZutatHinzugefuegt
ZutatEntfernt
AusbeuteGeaendert
```

---

### 2. Charge (Aggregate Root)

**Verantwortung:** Repräsentiert eine Produktions-Charge mit lückenloser Rückverfolgbarkeit.

**Struktur:**
```
Charge (Aggregate Root)
├── ChargenId (Wertobjekt) - Eindeutige Chargennummer (z.B. "CHARGE-2026-02-17-001")
├── RezeptId (Wertobjekt) - Referenz zu Rezept
├── Produktionsdatum (Wertobjekt)
├── GeplantemMenge (Wertobjekt)
├── TatsaechlicheMenge (Wertobjekt) - Nach Produktionsrückmeldung
├── Status (Wertobjekt: GEPLANT | IN_PRODUKTION | ABGESCHLOSSEN | STORNIERT)
├── ProduziertVon (Wertobjekt: BenutzerId)
├── ProduziertIn (Wertobjekt: FilialId) - Für Mehrfilialen
├── Verfallsdatum (Wertobjekt) - MHD des Endprodukts
├── VerwendeteZutatenchargen[] (Entität) - KRITISCH für Rückverfolgbarkeit!
│   ├── RohstoffChargenId (Wertobjekt) - Charge des verwendeten Rohstoffs
│   ├── ArtikelId (Wertobjekt)
│   ├── LieferantenChargennummer (Wertobjekt) - Vom Wareneingang
│   ├── VerwendeteMenge (Wertobjekt)
│   └── Verfallsdatum (Wertobjekt) - MHD des Rohstoffs
├── Ausschuss (Wertobjekt) - Ausschuss/Schwund
└── Bemerkungen (Wertobjekt) - Optional, für Abweichungen

Invarianten:
- Charge muss ein gültiges Rezept referenzieren
- TatsaechlicheMenge muss <= (GeplantemMenge / Rezept.AusbeuteInProzent) sein
- Status kann nur übergehen: GEPLANT → IN_PRODUKTION → ABGESCHLOSSEN (oder STORNIERT)
- Kann nicht abgeschlossen werden ohne verwendete Zutatenchargen zu erfassen
- Verfallsdatum muss in der Zukunft liegen zum Produktionszeitpunkt
- Ausschuss muss >= 0 sein
- Alle VerwendeteZutatenchargen müssen zu Rezept.Zutaten passen
```

**Geschäftsmethoden:**
```java
// Charge planen
public static Result<ChargenFehler, Charge> planen(
    RezeptId rezeptId,
    Menge geplantemMenge,
    LocalDate produktionsdatum,
    LocalDate verfallsdatum,
    BenutzerId produziertVon,
    FilialId filialId
);

// Produktion starten
public Result<ChargenFehler, Void> produktionStarten();

// Zutatverbrauch erfassen
public Result<ChargenFehler, Void> zutatverbrauchErfassen(
    ArtikelId artikelId,
    ChargenId rohstoffChargenId,
    LieferantenChargennummer lieferantenChargennummer,
    Menge verwendeteMenge,
    LocalDate verfallsdatum
);

// Charge abschließen
public Result<ChargenFehler, Void> abschliessen(
    Menge tatsaechlicheMenge,
    Menge ausschuss,
    Optional<String> bemerkungen
);

// Charge stornieren
public Result<ChargenFehler, Void> stornieren(String grund);

// Abfragemethoden
public boolean istRueckverfolgbar(); // Alle Zutaten haben Chargennummern
public List<ChargenId> holeVorgelageteChargen(); // Alle verwendeten Rohstoffchargen
public Menge berechneAusbeuteEffizienz(); // TatsaechlicheMenge / (GeplantemMenge / AusbeuteInProzent)
```

**Domänen-Events:**
```java
ChargeGeplant
ChargeGestartet
ZutatverbrauchErfasst
ChargeAbgeschlossen(ChargenId, ArtikelId, Menge, Verfallsdatum) → löst Bestandszugang aus
ChargeStorniert
```

---

### 3. Produktionsauftrag (Aggregate Root)

**Verantwortung:** Plant eine zukünftige Produktion.

**Struktur:**
```
Produktionsauftrag (Aggregate Root)
├── ProduktionsauftragId (Wertobjekt)
├── RezeptId (Wertobjekt)
├── GeplantemMenge (Wertobjekt)
├── GeplantesDatum (Wertobjekt)
├── Prioritaet (Wertobjekt: NIEDRIG | NORMAL | HOCH | DRINGEND)
├── Status (Wertobjekt: GEPLANT | FREIGEGEBEN | IN_PRODUKTION | ABGESCHLOSSEN | STORNIERT)
├── ErstelltVon (Wertobjekt: BenutzerId)
├── ErstelltAm (Wertobjekt: Zeitstempel)
├── Zielfiliale (Wertobjekt: FilialId) - Für Mehrfilialen-Produktion
├── ErzeugeChargenId (Wertobjekt) - Link zur tatsächlichen Charge bei Produktionsstart
└── Bemerkungen (Wertobjekt)

Invarianten:
- Geplante Menge muss positiv sein
- Geplantes Datum darf nicht in der Vergangenheit liegen
- Kann nur freigegeben werden, wenn Materialien verfügbar sind (geprüft in Anwendungsschicht!)
- Kann nicht abgeschlossen werden ohne eine Charge zu erzeugen
- Status-Übergänge: GEPLANT → FREIGEGEBEN → IN_PRODUKTION → ABGESCHLOSSEN
```

**Geschäftsmethoden:**
```java
// Produktionsauftrag erstellen
public static Result<ProduktionsauftragFehler, Produktionsauftrag> erstellen(
    RezeptId rezeptId,
    Menge geplantemMenge,
    LocalDate geplantesDatum,
    Prioritaet prioritaet,
    FilialId zielfiliale,
    BenutzerId erstelltVon
);

// Freigeben
public Result<ProduktionsauftragFehler, Void> freigeben();

// Produktion starten
public Result<ProduktionsauftragFehler, Void> produktionStarten(ChargenId chargenId);

// Abschließen
public Result<ProduktionsauftragFehler, Void> abschliessen();

// Stornieren
public Result<ProduktionsauftragFehler, Void> stornieren(String grund);

// Umplanen
public Result<ProduktionsauftragFehler, Void> umplanen(LocalDate neuesDatum);
```

**Domänen-Events:**
```java
ProduktionsauftragErstellt
ProduktionsauftragFreigegeben → löst Bedarfsplanung-Update aus
ProduktionsauftragGestartet
ProduktionsauftragAbgeschlossen
ProduktionsauftragStorniert
ProduktionsauftragUmgeplant
```

---

## Wertobjekte

### RezeptId
```java
public record RezeptId(String wert) {
    public RezeptId {
        if (wert == null || wert.isBlank()) {
            throw new IllegalArgumentException("RezeptId darf nicht leer sein");
        }
    }
}
```

### ChargenId
```java
public record ChargenId(String wert) {
    // Format: "CHARGE-YYYY-MM-DD-XXX"
    public static ChargenId generieren(LocalDate produktionsdatum, int sequenzNummer) {
        String wert = String.format("CHARGE-%s-%03d",
            produktionsdatum, sequenzNummer);
        return new ChargenId(wert);
    }
}
```

### AusbeuteInProzent
```java
public record AusbeuteInProzent(int wert) {
    public AusbeuteInProzent {
        if (wert < 1 || wert > 100) {
            throw new IllegalArgumentException(
                "Ausbeute muss zwischen 1-100% liegen, ist: " + wert
            );
        }
    }

    public Menge berechneBenoeteigtenInput(Menge gewuenschterOutput) {
        // Bei 80% Ausbeute und 100kg gewünschtem Output brauchen wir 125kg Input
        return gewuenschterOutput.multiplizieren(100.0 / wert);
    }
}
```

### RezeptTyp
```java
public enum RezeptTyp {
    ROHSTOFF,        // Rohstoff, kein Rezept
    ZWISCHENPRODUKT, // Zwischenprodukt (z.B. Gewürzmischung)
    ENDPRODUKT       // Endprodukt
}
```

---

## Domänen-Services

### RezeptValidator
```java
public class RezeptValidator {
    /**
     * Validiert, dass Rezept keine zyklischen Abhängigkeiten erzeugt.
     */
    public Result<RezeptFehler, Void> validiereKeineZyklischeAbhaengigkeit(
        Rezept rezept,
        RezeptRepository rezeptRepository
    );
}
```

### ChargenRueckverfolgbarkeitService
```java
public class ChargenRueckverfolgbarkeitService {
    /**
     * Findet alle vorgelagerten Chargen (Rohstoffe), die in einer Charge verwendet wurden.
     */
    public List<ChargenId> findeVorgelageteChargen(ChargenId chargenId);

    /**
     * Findet alle nachgelagerten Chargen (Endprodukte), die einen Rohstoff verwendet haben.
     * KRITISCH für Rückrufe!
     */
    public List<ChargenId> findeNachgelageteChargen(ChargenId rohstoffChargenId);
}
```

---

## Repository-Schnittstellen

```java
package com.effigenix.domain.produktion;

import com.effigenix.shared.result.Result;

public interface RezeptRepository {
    Result<RepositoryFehler, Void> speichern(Rezept rezept);
    Result<RepositoryFehler, Rezept> findeNachId(RezeptId id);
    Result<RepositoryFehler, List<Rezept>> findeAktive();
    Result<RepositoryFehler, List<Rezept>> findeNachArtikelId(ArtikelId artikelId);
}

public interface ChargenRepository {
    Result<RepositoryFehler, Void> speichern(Charge charge);
    Result<RepositoryFehler, Charge> findeNachId(ChargenId id);
    Result<RepositoryFehler, List<Charge>> findeNachProduktionsdatum(LocalDate datum);
    Result<RepositoryFehler, List<Charge>> findeNachStatus(ChargenStatus status);

    // Für Rückverfolgbarkeit
    Result<RepositoryFehler, List<Charge>> findeNachVorgelagerterCharge(ChargenId vorgelagertChargenId);
}

public interface ProduktionsauftragRepository {
    Result<RepositoryFehler, Void> speichern(Produktionsauftrag auftrag);
    Result<RepositoryFehler, Produktionsauftrag> findeNachId(ProduktionsauftragId id);
    Result<RepositoryFehler, List<Produktionsauftrag>> findeNachGeplantemDatum(LocalDate datum);
    Result<RepositoryFehler, List<Produktionsauftrag>> findeNachStatus(ProduktionsauftragStatus status);
}
```

---

## Domänen-Fehler

```java
public sealed interface RezeptFehler permits
    RezeptFehler.UngueltigeAusbeute,
    RezeptFehler.ZyklischeAbhaengigkeitEntdeckt,
    RezeptFehler.KeineZutatenVorhanden,
    RezeptFehler.RezeptNichtGefunden {
    String nachricht();
}

public sealed interface ChargenFehler permits
    ChargenFehler.UngueltigeMenge,
    ChargenFehler.UngueltigerStatusUebergang,
    ChargenFehler.FehlendeZutatenchargen,
    ChargenFehler.VerfallsdatumInVergangenheit,
    ChargenFehler.RezeptNichtGefunden {
    String nachricht();
}

public sealed interface ProduktionsauftragFehler permits
    ProduktionsauftragFehler.GeplantesDatumInVergangenheit,
    ProduktionsauftragFehler.UngueltigeMenge,
    ProduktionsauftragFehler.UngueltigerStatusUebergang,
    ProduktionsauftragFehler.RezeptNichtGefunden {
    String nachricht();
}
```

---

## Integration mit anderen Kontexten

### Upstream-Abhängigkeiten
- **Stammdaten-Kontext:** Rezept referenziert ArtikelId
- **Benutzerverwaltung:** Charge/Produktionsauftrag referenziert BenutzerId
- **Filialen-Kontext:** Produktionsauftrag referenziert FilialId

### Downstream-Integrationen
- **Bestandsführung:** `ChargeAbgeschlossen` Event löst Bestandszugang aus
- **Deklaration:** Deklaration liest Rezeptdaten für Nährwertberechnung
- **Beschaffung:** Produktionsauftrag löst Bedarfsplanung aus

---

## Anwendungsfälle (Anwendungsschicht)

```java
// application/produktion/RezeptErstellen.java
public class RezeptErstellen {
    public Result<AnwendungsFehler, RezeptDTO> ausfuehren(RezeptErstellenKommando cmd);
}

// application/produktion/ProduktionPlanen.java
public class ProduktionPlanen {
    public Result<AnwendungsFehler, ProduktionsauftragDTO> ausfuehren(ProduktionPlanenKommando cmd);
}

// application/produktion/ProduktionschargeStarten.java
public class ProduktionschargeStarten {
    public Result<AnwendungsFehler, ChargenDTO> ausfuehren(ChargeStartenKommando cmd);
}

// application/produktion/ProduktionschargeAbschliessen.java
public class ProduktionschargeAbschliessen {
    public Result<AnwendungsFehler, ChargenDTO> ausfuehren(ChargeAbschliessenKommando cmd);
    // Löst ChargeAbgeschlossen Event aus → Bestandszugang
}
```

---

## Beispiel: Chargen-Erstellungs-Ablauf

```java
// 1. Produktionsauftrag planen
Produktionsauftrag auftrag = Produktionsauftrag.erstellen(
    rezeptId,
    Menge.von(100, "kg"),
    LocalDate.now().plusDays(1),
    Prioritaet.NORMAL,
    filialId,
    benutzerId
);

// 2. Auftrag freigeben (prüft Materialverfügbarkeit in Anwendungsschicht)
auftrag.freigeben();

// 3. Produktion starten → Charge erstellen
Charge charge = Charge.planen(
    auftrag.rezeptId(),
    auftrag.geplantemMenge(),
    LocalDate.now(),
    LocalDate.now().plusDays(30), // MHD
    benutzerId,
    filialId
);
charge.produktionStarten();
auftrag.produktionStarten(charge.id());

// 4. Zutatverbrauch erfassen
charge.zutatverbrauchErfassen(
    ArtikelId.von("ART-001"),
    ChargenId.von("CHARGE-2026-02-15-042"), // Lieferanten-Charge
    LieferantenChargennummer.von("SUPPLIER-12345"),
    Menge.von(50, "kg"),
    LocalDate.now().plusDays(20) // MHD des Rohstoffs
);

// 5. Charge abschließen
charge.abschliessen(
    Menge.von(80, "kg"), // Tatsächlicher Output
    Menge.von(5, "kg"),  // Ausschuss
    Optional.of("Leichter Schwund beim Räuchern")
);
auftrag.abschliessen();

// 6. ChargeAbgeschlossen Event → Bestandsführung erzeugt Bestandseintrag
```
