# 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 create(RecipeDraft draft); // Mutations (nur im DRAFT-Status) public Result addIngredient(IngredientDraft draft); public Result removeIngredient(IngredientId ingredientId); public Result addProductionStep(ProductionStepDraft draft); public Result removeProductionStep(int stepNumber); // Status-Übergänge public Result activate(); public Result archive(); // Query Methods public Quantity calculateRequiredInput(Quantity desiredOutput); public LocalDate calculateBestBeforeDate(LocalDate productionDate); public List 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 plan(BatchDraft draft); // Status-Übergänge public Result startProduction(); public Result complete(Quantity actualQuantity, Quantity waste, String remarks); public Result cancel(String reason); // Genealogie (nur in IN_PRODUCTION) public Result recordConsumption(ConsumptionDraft draft); // Query Methods public boolean isFullyTraceable(); public List 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 create(ProductionOrderDraft draft); // Status-Übergänge public Result release(); public Result startProduction(BatchId batchId); public Result complete(); public Result cancel(String reason); // Mutations public Result 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 { <> KILOGRAM GRAM LITER MILLILITER PIECE METER } class YieldPercentage { +int value +calculateRequiredInput(Quantity) Quantity } class BatchNumber { +String value +generate(LocalDate, int) BatchNumber } class RecipeType { <> RAW_MATERIAL INTERMEDIATE FINISHED_PRODUCT } class Priority { <> LOW NORMAL HIGH URGENT } class RecipeStatus { <> DRAFT ACTIVE ARCHIVED } class BatchStatus { <> PLANNED IN_PRODUCTION COMPLETED CANCELLED } class ProductionOrderStatus { <> 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 of(BigDecimal amount, UnitOfMeasure uom); // Factory: Dual-Quantity (Catch-Weight) public static Result 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 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 traceForward(BatchId sourceBatchId); /** * Rückwärts-Tracing: Welche Rohstoff-Chargen stecken in diesem Endprodukt? */ public List 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 findById(RecipeId id); List findByStatus(RecipeStatus status); List findByArticleId(ArticleId articleId); List findActiveByIngredientArticleId(ArticleId ingredientArticleId); void save(Recipe recipe); boolean existsByNameAndVersion(String name, int version); } public interface BatchRepository { Optional findById(BatchId id); Optional findByBatchNumber(BatchNumber batchNumber); List findByProductionDate(LocalDate date); List findByStatus(BatchStatus status); List findByArticleId(ArticleId articleId); // Für Rückverfolgbarkeit List findByInputBatchId(BatchId upstreamBatchId); void save(Batch batch); } public interface ProductionOrderRepository { Optional findById(ProductionOrderId id); List findByPlannedDate(LocalDate date); List findByPlannedDateRange(LocalDate from, LocalDate to); List 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() StartOrderProduction → order.startProduction(batchId) + Batch.plan() 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 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)