1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 14:19:35 +01:00
effigenix/docs/mvp/ddd/07-bestandsfuehrungs-kontext.md
Sebastian Frick 4e448afa57 init
2026-02-17 08:25:06 +01:00

5.3 KiB

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:

// Bestand hinzufügen
public Result<BestandFehler, Void> hinzufuegen(
    ChargenId chargenId,
    ChargenTyp chargenTyp,
    Menge menge,
    LocalDate verfallsdatum
);

// Bestand abheben (mit FEFO)
public Result<BestandFehler, List<ChargenAbhebung>> abheben(Menge menge);

// Charge reservieren
public Result<BestandFehler, Void> reservieren(ChargenId chargenId, Menge menge);

// Abgelaufene Chargen markieren
public Result<BestandFehler, List<ChargenId>> markiereAbgelaufeneChargen();

// Abfragemethoden
public Menge verfuegbareMenge();
public List<Charge> findeChargenNachFEFO(); // Sortiert nach Verfallsdatum
public boolean istUnterMinimalBestand();

Domänen-Events:

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:

BestandsbewegungErfasst(BestandsbewegungId, ChargenId, BewegungsTyp)

Repository-Schnittstellen

public interface BestandRepository {
    Result<RepositoryFehler, Void> speichern(Bestand bestand);
    Result<RepositoryFehler, Bestand> findeNachArtikelUndLagerort(
        ArtikelId artikelId,
        LagerortId lagerortId,
        FilialId filialId
    );
    Result<RepositoryFehler, List<Bestand>> findeUnterMinimalBestand();
}

public interface BestandsbewegungRepository {
    Result<RepositoryFehler, Void> speichern(Bestandsbewegung bewegung);
    Result<RepositoryFehler, List<Bestandsbewegung>> findeNachCharge(ChargenId chargenId);
    Result<RepositoryFehler, List<Bestandsbewegung>> findeNachZeitraum(
        LocalDate von, LocalDate bis
    );
}

Rückverfolgbarkeits-Beispiel

// Beispiel: Rückruf wegen kontaminierter Rohstoff-Charge

// 1. Finde alle Bestandsbewegungen für Lieferanten-Charge
List<Bestandsbewegung> wareneingang = repository.findeNachCharge(
    ChargenId.von("SUPPLIER-BATCH-12345")
);

// 2. Finde Produktionschargen, die diesen Rohstoff verwendet haben
List<ChargenId> produktionschargen = wareneingang.stream()
    .filter(b -> b.bewegungsTyp() == BewegungsTyp.PRODUKTIONSAUSGANG)
    .map(b -> b.referenzdokument().produktionsChargenId())
    .toList();

// 3. Finde alle Verkäufe dieser Produktionschargen
List<Bestandsbewegung> verkaeufe = produktionschargen.stream()
    .flatMap(chargeId -> repository.findeNachCharge(chargeId).stream())
    .filter(b -> b.bewegungsTyp() == BewegungsTyp.VERKAUF)
    .toList();

// 4. Extrahiere betroffene Kunden/Rechnungen
List<RechnungId> betroffeneRechnungen = verkaeufe.stream()
    .map(b -> b.referenzdokument().rechnungId())
    .toList();

// → Rückruf kann durchgeführt werden!