# Bestandsführungs-Kontext - Detailliertes Domain Model **Bounded Context:** Bestandsführung **Domain-Typ:** KERN **Verantwortung:** Chargen-basierte Bestandsführung, Rückverfolgbarkeit, MHD-Tracking --- ## Aggregate ### 1. Bestand (Aggregate Root) **Struktur:** ``` Bestand (Aggregate Root) ├── BestandId (Wertobjekt) ├── ArtikelId (Wertobjekt) - Referenz zu Stammdaten ├── LagerortId (Wertobjekt) ├── FilialId (Wertobjekt) ├── Bestandsmenge (Wertobjekt: Menge) - Aktueller Gesamtbestand ├── Chargen[] (Entität) - Chargen-spezifischer Bestand (KRITISCH!) │ ├── ChargenId (Wertobjekt) - ProduktionsChargenId ODER LieferantenChargenId │ ├── ChargenTyp (Wertobjekt: PRODUZIERT | EINGEKAUFT) │ ├── Menge (Wertobjekt) │ ├── Verfallsdatum (Wertobjekt: MHD) │ ├── EingegangenenAm (Wertobjekt) │ └── Status (Wertobjekt: VERFUEGBAR | RESERVIERT | ABGELAUFEN | VERKAUFT) ├── MinimalBestandsmenge (Wertobjekt) - Für Warnungen bei niedrigem Bestand └── Nachbestellpunkt (Wertobjekt) - Auslöser für Beschaffung Invarianten: - Bestandsmenge = SUMME(Chargen.Menge wo Status = VERFUEGBAR) - Kann nicht mehr abheben als verfügbar - FEFO durchgesetzt: Ältestes Verfallsdatum zuerst - Negativer Bestand nicht erlaubt - Chargen mit Verfallsdatum < HEUTE müssen Status = ABGELAUFEN haben ``` **Geschäftsmethoden:** ```java // Bestand hinzufügen public Result hinzufuegen( ChargenId chargenId, ChargenTyp chargenTyp, Menge menge, LocalDate verfallsdatum ); // Bestand abheben (mit FEFO) public Result> abheben(Menge menge); // Charge reservieren public Result reservieren(ChargenId chargenId, Menge menge); // Abgelaufene Chargen markieren public Result> markiereAbgelaufeneChargen(); // Abfragemethoden public Menge verfuegbareMenge(); public List findeChargenNachFEFO(); // Sortiert nach Verfallsdatum public boolean istUnterMinimalBestand(); ``` **Domänen-Events:** ```java BestandUnterMinimum(ArtikelId, FilialId, Menge) ChargeLaeuftDemnaechstAb(ChargenId, ArtikelId, Verfallsdatum) ``` --- ### 2. Bestandsbewegung (Aggregate Root) **Event Sourcing Kandidat!** **Struktur:** ``` Bestandsbewegung (Aggregate Root) ├── BestandsbewegungId (Wertobjekt) ├── ArtikelId (Wertobjekt) ├── ChargenId (Wertobjekt) - KRITISCH für Rückverfolgbarkeit ├── BewegungsTyp (Wertobjekt: WARENEINGANG | PRODUKTIONSAUSGANG | VERKAUF | │ INTERFILIAL_TRANSFER | AUSSCHUSS | INVENTUR_KORREKTUR) ├── VonLagerort (Wertobjekt: LagerortId) - Null bei WARENEINGANG ├── ZuLagerort (Wertobjekt: LagerortId) - Null bei VERKAUF/AUSSCHUSS ├── VonFiliale (Wertobjekt: FilialId) ├── ZuFiliale (Wertobjekt: FilialId) - Für Interfilial-Transfers ├── Menge (Wertobjekt) ├── Bewegungsdatum (Wertobjekt: Zeitstempel) ├── DurchgefuehrtVon (Wertobjekt: BenutzerId) ├── Grund (Wertobjekt: für AUSSCHUSS/KORREKTUR) ├── Referenzdokument (Wertobjekt) - WareneingangId, ProduktionsauftragId, RechnungId └── Rueckverfolgbarkeitskette (Wertobjekt) - Link zu vorgelagerten/nachgelagerten Bewegungen Invarianten: - Menge muss positiv sein - WARENEINGANG: VonLagerort muss null sein - VERKAUF oder AUSSCHUSS: ZuLagerort muss null sein - INTERFILIAL_TRANSFER: VonFiliale != ZuFiliale - AUSSCHUSS benötigt Grund - Alle Bewegungen müssen eine Charge referenzieren ``` **Domänen-Events:** ```java BestandsbewegungErfasst(BestandsbewegungId, ChargenId, BewegungsTyp) ``` --- ## Repository-Schnittstellen ```java public interface BestandRepository { Result speichern(Bestand bestand); Result findeNachArtikelUndLagerort( ArtikelId artikelId, LagerortId lagerortId, FilialId filialId ); Result> findeUnterMinimalBestand(); } public interface BestandsbewegungRepository { Result speichern(Bestandsbewegung bewegung); Result> findeNachCharge(ChargenId chargenId); Result> findeNachZeitraum( LocalDate von, LocalDate bis ); } ``` --- ## Rückverfolgbarkeits-Beispiel ```java // Beispiel: Rückruf wegen kontaminierter Rohstoff-Charge // 1. Finde alle Bestandsbewegungen für Lieferanten-Charge List wareneingang = repository.findeNachCharge( ChargenId.von("SUPPLIER-BATCH-12345") ); // 2. Finde Produktionschargen, die diesen Rohstoff verwendet haben List produktionschargen = wareneingang.stream() .filter(b -> b.bewegungsTyp() == BewegungsTyp.PRODUKTIONSAUSGANG) .map(b -> b.referenzdokument().produktionsChargenId()) .toList(); // 3. Finde alle Verkäufe dieser Produktionschargen List verkaeufe = produktionschargen.stream() .flatMap(chargeId -> repository.findeNachCharge(chargeId).stream()) .filter(b -> b.bewegungsTyp() == BewegungsTyp.VERKAUF) .toList(); // 4. Extrahiere betroffene Kunden/Rechnungen List betroffeneRechnungen = verkaeufe.stream() .map(b -> b.referenzdokument().rechnungId()) .toList(); // → Rückruf kann durchgeführt werden! ```