1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 17:29:58 +01:00
effigenix/backend/docs/mvp/ddd/04-production-bc.md
Sebastian Frick bfae3eff73 feat(production): Produktion über Auftrag starten (US-P15)
RELEASED ProductionOrder kann mit einer PLANNED Batch verknüpft und
in Produktion gestartet werden. Dabei wechselt der Order auf IN_PROGRESS
und die Batch auf IN_PRODUCTION. Neuer REST-Endpoint POST /{id}/start,
StartOrderProduction Use Case, BatchAlreadyAssigned Error, Liquibase-
Migration für batch_id FK auf production_orders.
2026-02-25 08:53:17 +01:00

1126 lines
35 KiB
Markdown

# 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)