1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:40:18 +01:00
effigenix/backend/docs/mvp/ddd/05-quality-bc.md
Sebastian Frick fb735702cf docs(ddd): detaillierte Domain Models für Production, Quality und Inventory BC
- Production BC: Aggregates (Recipe, Batch, ProductionOrder) mit Invarianten, Drafts, Status-Maschinen, Domain Events und Chargen-Genealogie
- Quality BC: 9 Aggregates (TemperatureLog, CleaningPlan/Record, GoodsReceiptInspection, SampleRecord, TrainingRecord, MaintenanceRecord, QualityHold, ProcessParameter) mit HACCP-Compliance
- Inventory BC: 4 Aggregates (Stock, StockMovement, InventoryCount, StorageLocation) mit FEFO, Reservierungen mit Priorität, Vier-Augen-Prinzip bei Inventur
- Ubiquitous Language: Inventory-Sektion von 11 auf 27 Begriffe erweitert
- Alte deutsche Datei 05-qualitaets-kontext.md entfernt (ersetzt durch 05-quality-bc.md)
2026-02-19 01:13:12 +01:00

1849 lines
62 KiB
Markdown

# Quality BC (HACCP/QM) - Detailliertes Domain Model
**Bounded Context:** Quality
**Domain Type:** CORE
**Verantwortung:** HACCP-Compliance, Qualitätsmanagement, Audit-Vorbereitung, lückenlose Dokumentation aller qualitätsrelevanten Prozesse
---
## Ubiquitous Language
| Begriff (DE) | Begriff (EN/Code) | Typ | Definition |
|---|---|---|---|
| Temperaturprotokoll | TemperatureLog | Aggregate | Standortbezogene Temperaturmessung an kritischem Punkt (Kühlraum, Theke) mit Grenzwertüberwachung |
| Messpunkt | MeasurementPoint | VO (Enum) | Physischer Ort der Messung: COLD_ROOM, FREEZER, DISPLAY_COUNTER, PRODUCTION_ROOM |
| Temperatur | Temperature | VO | Temperaturwert in °C mit physikalisch plausiblem Bereich (-50 bis +50) |
| Kritischer Grenzwert | CriticalLimit | VO | Min-/Max-Paar für einen CCP (z.B. Kühlraum 2-7°C) |
| Reinigungsplan | CleaningPlan | Aggregate | Vorlage für Reinigungsaufgaben mit Intervall, Bereich und Checkliste |
| Reinigungsintervall | CleaningInterval | VO (Enum) | Turnus: DAILY, WEEKLY, MONTHLY |
| Reinigungsnachweis | CleaningRecord | Aggregate | Durchgeführte Reinigung gegen einen CleaningPlan mit Checkliste und Nachweis |
| Checklisten-Eintrag | ChecklistItem | Entity | Einzelner Prüfpunkt in einer Reinigung oder Inspektion |
| Wareneingangskontrolle | GoodsReceiptInspection | Aggregate | Mehrteilige Prüfung bei Warenanlieferung (Temperatur, Sicht, MHD, Dokumente) |
| Temperaturprüfung | TemperatureCheck | VO | Temperaturmessung bei Wareneingang mit Soll-/Ist-Vergleich |
| Sichtkontrolle | VisualCheck | VO | Prüfung von Verpackung, Farbe, Geruch |
| MHD-Prüfung | ShelfLifeCheck | VO | Prüfung des Mindesthaltbarkeitsdatums gegen Mindest-Restlaufzeit |
| Dokumentenprüfung | DocumentCheck | VO | Prüfung von Lieferschein, Veterinärbescheinigung, Zertifikaten |
| Probenentnahme | SampleRecord | Aggregate | Probenentnahme mit Analyseergebnis, Charge und Prüfmethode |
| Analyseergebnis | AnalysisResult | VO | Messergebnis einer Probe mit Einheit und Bewertung |
| Schulungsnachweis | TrainingRecord | Aggregate | Nachweis einer absolvierten Schulung mit Gültigkeitsdauer |
| Schulungsart | TrainingType | VO (Enum) | Klassifizierung: HACCP, HYGIENE, FOOD_SAFETY, EQUIPMENT_OPERATION, FIRST_AID |
| Wartungsprotokoll | MaintenanceRecord | Aggregate | Dokumentation einer Gerätewartung (planmäßig/Störung) mit Befund und Maßnahmen |
| Wartungsart | MaintenanceType | VO (Enum) | SCHEDULED, REPAIR, CALIBRATION, INSPECTION |
| Qualitätssperre | QualityHold | Aggregate | Sperre einer Charge mit Block/Release-Workflow und Begründung |
| Prozessparameter | ProcessParameter | Aggregate | Batch-bezogene CCP-Messwerte (Kerntemperatur, pH, aw-Wert) |
| CCP-Typ | CcpType | VO (Enum) | Typ des kritischen Kontrollpunkts: CORE_TEMPERATURE, PH_VALUE, WATER_ACTIVITY, METAL_DETECTION |
| Abweichung | Deviation | Concept | Überschreitung eines Grenzwerts oder Nichteinhaltung eines Verfahrens |
| Korrekturmaßnahme | CorrectiveAction | Entity | Maßnahme zur Behebung einer Abweichung mit Verantwortlichem und Frist |
| HACCP-Report | HaccpReport | Concept | Audit-Report aggregiert aus allen Quality-Aggregates |
---
## Aggregate-Übersicht
```mermaid
---
config:
theme: neutral
look: classic
layout: elk
themeVariables:
background: "#f8fafc"
class:
hideEmptyMembersBox: true
---
classDiagram
class TemperatureLog {
+TemperatureLogId id
+MeasurementPoint measurementPoint
+String deviceId
+Temperature temperature
+CriticalLimit criticalLimit
+TemperatureStatus status
+UserId measuredBy
+Instant measuredAt
+record(TemperatureLogDraft) Result~TemperatureLogError, TemperatureLog~
}
class CleaningPlan {
+CleaningPlanId id
+String name
+CleaningArea area
+CleaningInterval interval
+List~String~ checklistTemplate
+CleaningPlanStatus status
+create(CleaningPlanDraft) Result~CleaningPlanError, CleaningPlan~
+update(CleaningPlanUpdateDraft) Result~CleaningPlanError, Void~
+activate() Result~CleaningPlanError, Void~
+deactivate() Result~CleaningPlanError, Void~
}
class CleaningRecord {
+CleaningRecordId id
+CleaningPlanId cleaningPlanId
+LocalDate scheduledFor
+List~ChecklistItem~ checklistItems
+CleaningRecordStatus status
+UserId completedBy
+Instant completedAt
+create(CleaningRecordDraft) Result~CleaningRecordError, CleaningRecord~
+checkItem(int, String) Result~CleaningRecordError, Void~
+complete(UserId) Result~CleaningRecordError, Void~
}
class GoodsReceiptInspection {
+InspectionId id
+String goodsReceiptId
+String supplierBatchNumber
+TemperatureCheck temperatureCheck
+VisualCheck visualCheck
+ShelfLifeCheck shelfLifeCheck
+DocumentCheck documentCheck
+InspectionResult result
+UserId inspectedBy
+Instant inspectedAt
+create(GoodsReceiptInspectionDraft) Result~InspectionError, GoodsReceiptInspection~
+recordTemperatureCheck(TemperatureCheckDraft) Result~InspectionError, Void~
+recordVisualCheck(VisualCheckDraft) Result~InspectionError, Void~
+recordShelfLifeCheck(ShelfLifeCheckDraft) Result~InspectionError, Void~
+recordDocumentCheck(DocumentCheckDraft) Result~InspectionError, Void~
+finalize() Result~InspectionError, Void~
}
class SampleRecord {
+SampleRecordId id
+String batchId
+SampleType sampleType
+AnalysisResult analysisResult
+CriticalLimit criticalLimit
+SampleStatus status
+UserId sampledBy
+Instant sampledAt
+record(SampleRecordDraft) Result~SampleRecordError, SampleRecord~
+enterResult(AnalysisResultDraft) Result~SampleRecordError, Void~
}
class TrainingRecord {
+TrainingRecordId id
+UserId employeeId
+TrainingType trainingType
+LocalDate trainingDate
+LocalDate validUntil
+String trainer
+String certificateNumber
+TrainingStatus status
+record(TrainingRecordDraft) Result~TrainingRecordError, TrainingRecord~
+revoke(String) Result~TrainingRecordError, Void~
}
class MaintenanceRecord {
+MaintenanceRecordId id
+String equipmentId
+MaintenanceType maintenanceType
+LocalDate scheduledFor
+Instant performedAt
+String performedBy
+String findings
+String actionsTaken
+LocalDate nextMaintenanceDue
+MaintenanceStatus status
+schedule(MaintenanceRecordDraft) Result~MaintenanceRecordError, MaintenanceRecord~
+complete(MaintenanceCompletionDraft) Result~MaintenanceRecordError, Void~
+fail(String, String) Result~MaintenanceRecordError, Void~
}
class QualityHold {
+QualityHoldId id
+String batchId
+HoldReason reason
+String description
+QualityHoldStatus status
+UserId blockedBy
+Instant blockedAt
+UserId releasedBy
+Instant releasedAt
+String releaseJustification
+block(QualityHoldDraft) Result~QualityHoldError, QualityHold~
+release(UserId, String) Result~QualityHoldError, Void~
+reject(UserId, String) Result~QualityHoldError, Void~
}
class ProcessParameter {
+ProcessParameterId id
+String batchId
+CcpType ccpType
+BigDecimal measuredValue
+String unit
+CriticalLimit criticalLimit
+ParameterStatus status
+UserId measuredBy
+Instant measuredAt
+record(ProcessParameterDraft) Result~ProcessParameterError, ProcessParameter~
}
CleaningRecord --> CleaningPlan : referenziert
CleaningRecord "1" *-- "*" ChecklistItem : enthält
QualityHold --> Batch : sperrt
ProcessParameter --> Batch : misst
SampleRecord --> Batch : beprobt
```
---
## Aggregates
### 1. TemperatureLog (Aggregate Root)
**Verantwortung:** Standortbezogene Temperaturprotokollierung für Kühlräume, Tiefkühler, Theken und Produktionsräume. Automatische Status-Ermittlung gegen kritische Grenzwerte (Epic 3.1).
```
TemperatureLog (Aggregate Root)
├── TemperatureLogId (VO)
├── MeasurementPoint (VO: COLD_ROOM | FREEZER | DISPLAY_COUNTER | PRODUCTION_ROOM)
├── DeviceId (VO: String) - Referenz auf Messgerät/Standort
├── Temperature (VO) - Gemessener Wert in °C
├── CriticalLimit (VO) - Min/Max-Grenzwerte für diesen Messpunkt
├── Status (VO: OK | WARNING | CRITICAL) - Automatisch berechnet
├── MeasuredBy (VO: UserId)
├── MeasuredAt (VO: Instant)
└── Remarks (VO: String) - Optional, bei Abweichungen
```
**Invarianten:**
```java
/**
* TemperatureLog aggregate root.
*
* Invariants:
* - Temperature must be within physically possible range (-50°C to +50°C)
* - MeasuredAt cannot be in the future
* - CriticalLimit.min < CriticalLimit.max
* - Status is automatically derived:
* CRITICAL if temperature outside critical limits
* WARNING if temperature within 10% of limit boundary
* OK otherwise
* - Immutable after creation (append-only log)
*/
```
**Draft-Record:**
```java
public record TemperatureLogDraft(
String measurementPoint, // COLD_ROOM | FREEZER | DISPLAY_COUNTER | PRODUCTION_ROOM
String deviceId,
String temperature, // BigDecimal als String (°C)
String criticalLimitMin, // BigDecimal als String (°C)
String criticalLimitMax, // BigDecimal als String (°C)
String measuredBy, // UserId
String remarks // nullable
) {}
```
**Factory & Business Methods:**
```java
// Factory (einzige Erzeugung, immutable danach)
public static Result<TemperatureLogError, TemperatureLog> record(TemperatureLogDraft draft);
// Query Methods
public boolean isCritical();
public boolean isWarning();
public BigDecimal deviationFromLimit(); // Abweichung vom nächsten Grenzwert
```
**Domain Events:**
```java
TemperatureLogged(TemperatureLogId, MeasurementPoint, Temperature, TemperatureStatus)
TemperatureCriticalLimitExceeded(TemperatureLogId, MeasurementPoint, Temperature, CriticalLimit)
```
---
### 2. CleaningPlan (Aggregate Root)
**Verantwortung:** Vorlage für wiederkehrende Reinigungsaufgaben mit Bereich, Intervall und Checklisten-Template. Bildet die Grundlage für CleaningRecords (Epic 3.2).
```
CleaningPlan (Aggregate Root)
├── CleaningPlanId (VO)
├── Name (VO: String) - Bezeichnung (z.B. "Tägliche Reinigung Kühlraum 1")
├── Area (VO: PRODUCTION_ROOM | COLD_STORAGE | SALES_COUNTER | EQUIPMENT | VEHICLE)
├── Interval (VO: DAILY | WEEKLY | MONTHLY)
├── ChecklistTemplate[] (VO: List<String>) - Vorlagen-Checkliste
├── Status (VO: ACTIVE | INACTIVE)
├── CreatedBy (VO: UserId)
└── CreatedAt (VO: Instant)
```
**Invarianten:**
```java
/**
* CleaningPlan aggregate root.
*
* Invariants:
* - Name must not be blank
* - ChecklistTemplate must have at least one item
* - Only ACTIVE plans can generate CleaningRecords
* - Status transitions: ACTIVE ↔ INACTIVE (bidirectional)
* - ChecklistTemplate items must not be blank
*/
```
**Draft-Records:**
```java
public record CleaningPlanDraft(
String name,
String area, // PRODUCTION_ROOM | COLD_STORAGE | SALES_COUNTER | EQUIPMENT | VEHICLE
String interval, // DAILY | WEEKLY | MONTHLY
List<String> checklistTemplate,
String createdBy // UserId
) {}
public record CleaningPlanUpdateDraft(
String name, // nullable = keine Änderung
String interval, // nullable
List<String> checklistTemplate // nullable
) {}
```
**Factory & Business Methods:**
```java
// Factory
public static Result<CleaningPlanError, CleaningPlan> create(CleaningPlanDraft draft);
// Mutations
public Result<CleaningPlanError, Void> update(CleaningPlanUpdateDraft draft);
public Result<CleaningPlanError, Void> activate();
public Result<CleaningPlanError, Void> deactivate();
// Query Methods
public boolean isDueOn(LocalDate date);
```
**Domain Events:**
```java
CleaningPlanCreated(CleaningPlanId, CleaningArea, CleaningInterval)
CleaningPlanDeactivated(CleaningPlanId)
```
---
### 3. CleaningRecord (Aggregate Root)
**Verantwortung:** Nachweis einer durchgeführten Reinigung gegen einen CleaningPlan. Enthält abgehakte Checkliste und optionale Anmerkungen (Epic 3.2).
```
CleaningRecord (Aggregate Root)
├── CleaningRecordId (VO)
├── CleaningPlanId (VO) - Referenz auf zugehörigen Plan
├── ScheduledFor (VO: LocalDate) - Geplanter Termin
├── ChecklistItems[] (Entity)
│ ├── ChecklistItemId (VO: int) - Position in der Liste
│ ├── Description (VO: String) - Aus CleaningPlan-Template kopiert
│ ├── Checked (boolean) - Abgehakt?
│ └── Remarks (VO: String) - Optional, bei Besonderheiten
├── Status (VO: OPEN | IN_PROGRESS | COMPLETED | OVERDUE)
├── CompletedBy (VO: UserId) - Wer hat die Reinigung durchgeführt?
├── CompletedAt (VO: Instant)
└── OverallRemarks (VO: String) - Optional
```
**Invarianten:**
```java
/**
* CleaningRecord aggregate root.
*
* Invariants:
* - Must reference an existing CleaningPlanId
* - ChecklistItems are initialized from CleaningPlan template at creation
* - All ChecklistItems must be checked before completion
* - CompletedBy and CompletedAt are set at completion
* - Cannot modify after COMPLETED
* - Status transitions: OPEN → IN_PROGRESS → COMPLETED
* OPEN → OVERDUE (system-triggered if past scheduledFor)
* OVERDUE → IN_PROGRESS → COMPLETED
* - ChecklistItemId (position) must be unique within the record
*/
```
**Draft-Record:**
```java
public record CleaningRecordDraft(
String cleaningPlanId,
String scheduledFor, // ISO LocalDate
List<String> checklistItems // Kopie aus CleaningPlan-Template
) {}
```
**Factory & Business Methods:**
```java
// Factory
public static Result<CleaningRecordError, CleaningRecord> create(CleaningRecordDraft draft);
// Mutations
public Result<CleaningRecordError, Void> checkItem(int position, String remarks);
public Result<CleaningRecordError, Void> complete(UserId completedBy);
public Result<CleaningRecordError, Void> markOverdue();
// Query Methods
public boolean isFullyChecked();
public int checkedCount();
public int totalCount();
```
**Domain Events:**
```java
CleaningRecordCreated(CleaningRecordId, CleaningPlanId, LocalDate scheduledFor)
CleaningRecordCompleted(CleaningRecordId, CleaningPlanId, UserId completedBy)
CleaningOverdue(CleaningRecordId, CleaningPlanId, LocalDate scheduledFor)
```
---
### 4. GoodsReceiptInspection (Aggregate Root)
**Verantwortung:** Mehrteilige Wareneingangsprüfung mit Temperatur-, Sicht-, MHD- und Dokumentencheck. Gesamtergebnis wird aus Einzelprüfungen abgeleitet (Epic 3.3).
```
GoodsReceiptInspection (Aggregate Root)
├── InspectionId (VO)
├── GoodsReceiptId (VO: String) - Referenz auf Procurement BC
├── SupplierBatchNumber (VO: String) - Lieferanten-Chargennummer (Rückverfolgbarkeit)
├── TemperatureCheck (VO)
│ ├── MeasuredTemperature (BigDecimal) - Ist-Temperatur
│ ├── ExpectedMin (BigDecimal) - Soll-Min
│ ├── ExpectedMax (BigDecimal) - Soll-Max
│ └── Passed (boolean)
├── VisualCheck (VO)
│ ├── PackagingIntact (boolean)
│ ├── ColorAppearance (NORMAL | ABNORMAL)
│ ├── SmellCheck (NORMAL | ABNORMAL)
│ ├── Remarks (String) - Optional
│ └── Passed (boolean)
├── ShelfLifeCheck (VO)
│ ├── ExpiryDate (LocalDate)
│ ├── MinimumAcceptableDays (int) - Mindest-Restlaufzeit
│ ├── ActualDaysRemaining (int)
│ └── Passed (boolean)
├── DocumentCheck (VO)
│ ├── DeliveryNoteReceived (boolean)
│ ├── VeterinaryCertificateRequired (boolean)
│ ├── VeterinaryCertificateReceived (boolean)
│ ├── QualityCertificateReceived (boolean)
│ └── Passed (boolean)
├── Result (VO: PENDING | ACCEPTED | REJECTED | CONDITIONALLY_ACCEPTED)
├── RejectionReason (VO: String) - Pflicht bei REJECTED
├── Conditions (VO: String) - Pflicht bei CONDITIONALLY_ACCEPTED
├── InspectedBy (VO: UserId)
└── InspectedAt (VO: Instant)
```
**Invarianten:**
```java
/**
* GoodsReceiptInspection aggregate root.
*
* Invariants:
* - All four checks must be recorded before finalize()
* - Result is PENDING until finalize()
* - If any check fails → Result cannot be ACCEPTED
* - If REJECTED → RejectionReason must not be blank
* - If CONDITIONALLY_ACCEPTED → Conditions must not be blank
* - TemperatureCheck: MeasuredTemperature must be within ExpectedMin/Max for Passed=true
* - ShelfLifeCheck: ActualDaysRemaining >= MinimumAcceptableDays for Passed=true
* - DocumentCheck: VeterinaryCertificateReceived required if VeterinaryCertificateRequired
* - Immutable after finalize() (ACCEPTED/REJECTED/CONDITIONALLY_ACCEPTED)
* - Status transitions: PENDING → ACCEPTED | REJECTED | CONDITIONALLY_ACCEPTED
*/
```
**Draft-Records:**
```java
public record GoodsReceiptInspectionDraft(
String goodsReceiptId,
String supplierBatchNumber,
String inspectedBy // UserId
) {}
public record TemperatureCheckDraft(
String measuredTemperature, // BigDecimal als String (°C)
String expectedMin, // BigDecimal als String
String expectedMax // BigDecimal als String
) {}
public record VisualCheckDraft(
boolean packagingIntact,
String colorAppearance, // NORMAL | ABNORMAL
String smellCheck, // NORMAL | ABNORMAL
String remarks // nullable
) {}
public record ShelfLifeCheckDraft(
String expiryDate, // ISO LocalDate
int minimumAcceptableDays
) {}
public record DocumentCheckDraft(
boolean deliveryNoteReceived,
boolean veterinaryCertificateRequired,
boolean veterinaryCertificateReceived,
boolean qualityCertificateReceived
) {}
```
**Factory & Business Methods:**
```java
// Factory
public static Result<InspectionError, GoodsReceiptInspection> create(
GoodsReceiptInspectionDraft draft
);
// Einzelne Checks aufnehmen (Reihenfolge egal)
public Result<InspectionError, Void> recordTemperatureCheck(TemperatureCheckDraft draft);
public Result<InspectionError, Void> recordVisualCheck(VisualCheckDraft draft);
public Result<InspectionError, Void> recordShelfLifeCheck(ShelfLifeCheckDraft draft);
public Result<InspectionError, Void> recordDocumentCheck(DocumentCheckDraft draft);
// Abschluss: leitet Result aus Einzelprüfungen ab
public Result<InspectionError, Void> finalize();
// Manuelles Override bei CONDITIONALLY_ACCEPTED
public Result<InspectionError, Void> acceptConditionally(String conditions);
public Result<InspectionError, Void> reject(String reason);
// Query Methods
public boolean allChecksRecorded();
public List<String> failedChecks();
```
**Domain Events:**
```java
GoodsReceiptInspectionCreated(InspectionId, String goodsReceiptId)
GoodsReceiptAccepted(InspectionId, String goodsReceiptId, String supplierBatchNumber)
GoodsReceiptRejected(InspectionId, String goodsReceiptId, String reason)
GoodsReceiptConditionallyAccepted(InspectionId, String goodsReceiptId, String conditions)
```
---
### 5. SampleRecord (Aggregate Root)
**Verantwortung:** Probenentnahme mit Analyseergebnis. Verknüpft mit einer Charge, Prüfmethode und kritischem Grenzwert. Ergebnis wird nachträglich eingetragen (Epic 3.4).
```
SampleRecord (Aggregate Root)
├── SampleRecordId (VO)
├── BatchId (VO: String) - Referenz auf Produktions-Charge
├── SampleType (VO: MICROBIOLOGICAL | CHEMICAL | PHYSICAL | SENSORY)
├── SampleDescription (VO: String) - Was wurde beprobt
├── AnalysisMethod (VO: String) - Prüfmethode (z.B. "PCR", "pH-Messung")
├── AnalysisResult (VO) - Nullable bis Ergebnis vorliegt
│ ├── MeasuredValue (BigDecimal)
│ ├── Unit (String) - z.B. "CFU/g", "pH", "%"
│ └── Interpretation (String) - Freitext-Bewertung
├── CriticalLimit (VO) - Grenzwert für Bewertung
├── Status (VO: PENDING | PASSED | FAILED)
├── SampledBy (VO: UserId)
├── SampledAt (VO: Instant)
├── ResultEnteredAt (VO: Instant) - Nullable
└── Remarks (VO: String) - Optional
```
**Invarianten:**
```java
/**
* SampleRecord aggregate root.
*
* Invariants:
* - BatchId must not be blank
* - SampleDescription must not be blank
* - Status is PENDING until AnalysisResult is entered
* - Status = PASSED if measuredValue within CriticalLimit
* - Status = FAILED if measuredValue outside CriticalLimit
* - AnalysisResult can only be entered once (immutable after)
* - ResultEnteredAt must be after SampledAt
* - CriticalLimit.min < CriticalLimit.max (if both set)
*/
```
**Draft-Records:**
```java
public record SampleRecordDraft(
String batchId,
String sampleType, // MICROBIOLOGICAL | CHEMICAL | PHYSICAL | SENSORY
String sampleDescription,
String analysisMethod,
String criticalLimitMin, // BigDecimal als String, nullable (einseitiger Grenzwert möglich)
String criticalLimitMax, // BigDecimal als String, nullable
String sampledBy // UserId
) {}
public record AnalysisResultDraft(
String measuredValue, // BigDecimal als String
String unit,
String interpretation // nullable
) {}
```
**Factory & Business Methods:**
```java
// Factory
public static Result<SampleRecordError, SampleRecord> record(SampleRecordDraft draft);
// Ergebnis nachtragen
public Result<SampleRecordError, Void> enterResult(AnalysisResultDraft draft);
// Query Methods
public boolean isPending();
public boolean hasPassed();
```
**Domain Events:**
```java
SampleRecorded(SampleRecordId, String batchId, SampleType)
SamplePassed(SampleRecordId, String batchId)
SampleFailed(SampleRecordId, String batchId, BigDecimal measuredValue, CriticalLimit limit)
```
---
### 6. TrainingRecord (Aggregate Root)
**Verantwortung:** Schulungsnachweise für Mitarbeiter mit Gültigkeitsdauer. Warnt bei ablaufenden Zertifikaten (Epic 3.5).
```
TrainingRecord (Aggregate Root)
├── TrainingRecordId (VO)
├── EmployeeId (VO: UserId) - Geschulter Mitarbeiter
├── TrainingType (VO: HACCP | HYGIENE | FOOD_SAFETY | EQUIPMENT_OPERATION | FIRST_AID)
├── TrainingDate (VO: LocalDate)
├── ValidUntil (VO: LocalDate) - Auffrischung notwendig
├── Trainer (VO: String) - Name des Trainers (intern/extern)
├── CertificateNumber (VO: String) - Optional
├── Status (VO: VALID | EXPIRING_SOON | EXPIRED | REVOKED)
├── RevokedReason (VO: String) - Pflicht bei REVOKED
└── RevokedAt (VO: Instant) - Optional
```
**Invarianten:**
```java
/**
* TrainingRecord aggregate root.
*
* Invariants:
* - ValidUntil must be after TrainingDate
* - Status is derived:
* VALID if ValidUntil > today + 30 days
* EXPIRING_SOON if ValidUntil <= today + 30 days and > today
* EXPIRED if ValidUntil <= today
* REVOKED if explicitly revoked
* - Cannot revoke without reason
* - Immutable after creation (except revoke)
* - TrainingDate cannot be in the future
* - Trainer must not be blank
*/
```
**Draft-Record:**
```java
public record TrainingRecordDraft(
String employeeId, // UserId
String trainingType, // HACCP | HYGIENE | FOOD_SAFETY | EQUIPMENT_OPERATION | FIRST_AID
String trainingDate, // ISO LocalDate
String validUntil, // ISO LocalDate
String trainer,
String certificateNumber // nullable
) {}
```
**Factory & Business Methods:**
```java
// Factory
public static Result<TrainingRecordError, TrainingRecord> record(TrainingRecordDraft draft);
// Mutations
public Result<TrainingRecordError, Void> revoke(String reason);
// Query Methods
public boolean isValid();
public boolean isExpiringSoon();
public boolean isExpired();
public long daysUntilExpiry();
```
**Domain Events:**
```java
TrainingRecorded(TrainingRecordId, UserId employeeId, TrainingType)
TrainingExpiringSoon(TrainingRecordId, UserId employeeId, LocalDate validUntil)
TrainingExpired(TrainingRecordId, UserId employeeId, TrainingType)
TrainingRevoked(TrainingRecordId, UserId employeeId, String reason)
```
---
### 7. MaintenanceRecord (Aggregate Root)
**Verantwortung:** Wartungsprotokolle für Geräte und Anlagen. Geplante Wartungen, Störungsreparaturen und Kalibrierungen (Epic 3.6).
```
MaintenanceRecord (Aggregate Root)
├── MaintenanceRecordId (VO)
├── EquipmentId (VO: String) - Referenz auf Gerät/Anlage
├── EquipmentName (VO: String) - Bezeichnung für Lesbarkeit
├── MaintenanceType (VO: SCHEDULED | REPAIR | CALIBRATION | INSPECTION)
├── ScheduledFor (VO: LocalDate)
├── Status (VO: SCHEDULED | COMPLETED | FAILED | OVERDUE)
├── PerformedAt (VO: Instant) - Nullable bis Durchführung
├── PerformedBy (VO: String) - Interner Mitarbeiter oder externe Firma
├── Findings (VO: String) - Befund, nullable
├── ActionsTaken (VO: String) - Durchgeführte Maßnahmen, nullable
├── NextMaintenanceDue (VO: LocalDate) - Nächster Termin, nullable
└── FailureReason (VO: String) - Pflicht bei FAILED
```
**Invarianten:**
```java
/**
* MaintenanceRecord aggregate root.
*
* Invariants:
* - EquipmentId and EquipmentName must not be blank
* - ScheduledFor must not be in the past at creation (except REPAIR type)
* - Status transitions: SCHEDULED → COMPLETED | FAILED
* SCHEDULED → OVERDUE (system-triggered)
* - If COMPLETED: PerformedAt, PerformedBy must be set
* - If FAILED: FailureReason, Findings must not be blank
* - If COMPLETED and MaintenanceType is SCHEDULED: NextMaintenanceDue should be set
* - PerformedAt must be >= ScheduledFor (for SCHEDULED type)
* - REPAIR type can be created for past dates (Störungs-Dokumentation)
*/
```
**Draft-Records:**
```java
public record MaintenanceRecordDraft(
String equipmentId,
String equipmentName,
String maintenanceType, // SCHEDULED | REPAIR | CALIBRATION | INSPECTION
String scheduledFor // ISO LocalDate
) {}
public record MaintenanceCompletionDraft(
String performedBy,
String findings, // nullable
String actionsTaken, // nullable
String nextMaintenanceDue // ISO LocalDate, nullable
) {}
```
**Factory & Business Methods:**
```java
// Factory
public static Result<MaintenanceRecordError, MaintenanceRecord> schedule(
MaintenanceRecordDraft draft
);
// Status-Übergänge
public Result<MaintenanceRecordError, Void> complete(MaintenanceCompletionDraft draft);
public Result<MaintenanceRecordError, Void> fail(String failureReason, String findings);
public Result<MaintenanceRecordError, Void> markOverdue();
// Query Methods
public boolean isOverdue();
```
**Domain Events:**
```java
MaintenanceScheduled(MaintenanceRecordId, String equipmentId, MaintenanceType, LocalDate)
MaintenanceCompleted(MaintenanceRecordId, String equipmentId, LocalDate nextDue)
MaintenanceFailed(MaintenanceRecordId, String equipmentId, String reason)
MaintenanceOverdue(MaintenanceRecordId, String equipmentId, LocalDate scheduledFor)
```
---
### 8. QualityHold (Aggregate Root)
**Verantwortung:** Sperre einer Charge bei Qualitätsproblemen. Block/Release-Pattern: Erst sperren, dann nach Prüfung freigeben oder endgültig ablehnen.
```
QualityHold (Aggregate Root)
├── QualityHoldId (VO)
├── BatchId (VO: String) - Gesperrte Produktions-Charge
├── Reason (VO: TEMPERATURE_DEVIATION | SAMPLE_FAILED | CONTAMINATION_SUSPECTED |
│ PROCESS_DEVIATION | CUSTOMER_COMPLAINT | REGULATORY)
├── Description (VO: String) - Freitext-Beschreibung des Problems
├── Status (VO: BLOCKED | RELEASED | REJECTED)
├── BlockedBy (VO: UserId) - Wer hat gesperrt
├── BlockedAt (VO: Instant)
├── ReleasedBy (VO: UserId) - Wer hat freigegeben/abgelehnt, nullable
├── ResolvedAt (VO: Instant) - Nullable
├── ReleaseJustification (VO: String) - Begründung bei RELEASED
├── RejectionJustification (VO: String) - Begründung bei REJECTED
└── CorrectiveActions[] (Entity)
├── CorrectiveActionId (VO: int)
├── Description (VO: String) - Was wurde getan
├── ResponsiblePerson (VO: String)
└── CompletedAt (VO: Instant) - Nullable
```
**Invarianten:**
```java
/**
* QualityHold aggregate root.
*
* Invariants:
* - BatchId must not be blank
* - Description must not be blank
* - Status transitions: BLOCKED → RELEASED | REJECTED (terminal states)
* - Release requires ReleaseJustification (must not be blank)
* - Rejection requires RejectionJustification (must not be blank)
* - ReleasedBy must differ from BlockedBy (Vier-Augen-Prinzip)
* - Cannot modify after RELEASED or REJECTED
* - CorrectiveActions can only be added in BLOCKED status
* - At least one CorrectiveAction must exist before release
*/
```
**Draft-Records:**
```java
public record QualityHoldDraft(
String batchId,
String reason, // TEMPERATURE_DEVIATION | SAMPLE_FAILED | ...
String description,
String blockedBy // UserId
) {}
public record CorrectiveActionDraft(
String description,
String responsiblePerson
) {}
```
**Factory & Business Methods:**
```java
// Factory
public static Result<QualityHoldError, QualityHold> block(QualityHoldDraft draft);
// Korrekturmaßnahmen (nur im BLOCKED-Status)
public Result<QualityHoldError, Void> addCorrectiveAction(CorrectiveActionDraft draft);
public Result<QualityHoldError, Void> completeCorrectiveAction(int actionId);
// Status-Übergänge (terminal)
public Result<QualityHoldError, Void> release(UserId releasedBy, String justification);
public Result<QualityHoldError, Void> reject(UserId rejectedBy, String justification);
// Query Methods
public boolean isBlocked();
public boolean hasOpenCorrectiveActions();
```
**Domain Events:**
```java
QualityHoldCreated(QualityHoldId, String batchId, HoldReason)
QualityHoldReleased(QualityHoldId, String batchId, UserId releasedBy, String justification)
QualityHoldRejected(QualityHoldId, String batchId, UserId rejectedBy, String justification)
→ triggers batch disposal / write-off in Inventory BC
CorrectiveActionAdded(QualityHoldId, String description)
```
---
### 9. ProcessParameter (Aggregate Root)
**Verantwortung:** Batch-bezogene CCP-Messwerte (Kerntemperatur, pH-Wert, aw-Wert, Metalldetektion). Automatische Bewertung gegen kritische Grenzwerte.
```
ProcessParameter (Aggregate Root)
├── ProcessParameterId (VO)
├── BatchId (VO: String) - Referenz auf Produktions-Charge
├── CcpType (VO: CORE_TEMPERATURE | PH_VALUE | WATER_ACTIVITY | METAL_DETECTION)
├── MeasuredValue (VO: BigDecimal)
├── Unit (VO: String) - z.B. "°C", "pH", "aw"
├── CriticalLimit (VO) - Min/Max-Grenzwerte
├── Status (VO: OK | DEVIATION)
├── MeasuredBy (VO: UserId)
├── MeasuredAt (VO: Instant)
├── ProductionStepReference (VO: String) - Optional, Bezug zum Produktionsschritt
└── Remarks (VO: String) - Optional, bei Abweichungen Pflicht
```
**Invarianten:**
```java
/**
* ProcessParameter aggregate root.
*
* Invariants:
* - BatchId must not be blank
* - MeasuredValue must not be null
* - CriticalLimit.min < CriticalLimit.max (if both set)
* - Status is automatically derived:
* OK if measuredValue within CriticalLimit
* DEVIATION if measuredValue outside CriticalLimit
* - If DEVIATION: Remarks must not be blank (Abweichung muss dokumentiert werden)
* - Unit must match CcpType convention (°C for CORE_TEMPERATURE, pH for PH_VALUE, etc.)
* - Immutable after creation (append-only measurement log)
* - MeasuredAt cannot be in the future
*/
```
**Draft-Record:**
```java
public record ProcessParameterDraft(
String batchId,
String ccpType, // CORE_TEMPERATURE | PH_VALUE | WATER_ACTIVITY | METAL_DETECTION
String measuredValue, // BigDecimal als String
String unit,
String criticalLimitMin, // BigDecimal als String, nullable
String criticalLimitMax, // BigDecimal als String, nullable
String measuredBy, // UserId
String productionStepReference, // nullable
String remarks // nullable, Pflicht bei Abweichung
) {}
```
**Factory & Business Methods:**
```java
// Factory (einzige Erzeugung, immutable danach)
public static Result<ProcessParameterError, ProcessParameter> record(
ProcessParameterDraft draft
);
// Query Methods
public boolean isDeviation();
public BigDecimal deviationFromLimit();
```
**Domain Events:**
```java
ProcessParameterRecorded(ProcessParameterId, String batchId, CcpType, ParameterStatus)
ProcessParameterDeviation(ProcessParameterId, String batchId, CcpType, BigDecimal measuredValue, CriticalLimit limit)
→ kann QualityHold auslösen (Application Layer Entscheidung)
```
---
## Shared Value Objects
```mermaid
---
config:
theme: neutral
look: classic
layout: elk
themeVariables:
background: "#f8fafc"
class:
hideEmptyMembersBox: true
---
classDiagram
class Temperature {
+BigDecimal value
+of(BigDecimal) Result
+isWithin(CriticalLimit) boolean
+deviationFrom(CriticalLimit) BigDecimal
}
class CriticalLimit {
+BigDecimal min
+BigDecimal max
+of(BigDecimal, BigDecimal) Result
+contains(BigDecimal) boolean
+warningZoneContains(BigDecimal) boolean
}
class MeasurementPoint {
<<enumeration>>
COLD_ROOM
FREEZER
DISPLAY_COUNTER
PRODUCTION_ROOM
}
class CleaningArea {
<<enumeration>>
PRODUCTION_ROOM
COLD_STORAGE
SALES_COUNTER
EQUIPMENT
VEHICLE
}
class CleaningInterval {
<<enumeration>>
DAILY
WEEKLY
MONTHLY
}
class SampleType {
<<enumeration>>
MICROBIOLOGICAL
CHEMICAL
PHYSICAL
SENSORY
}
class CcpType {
<<enumeration>>
CORE_TEMPERATURE
PH_VALUE
WATER_ACTIVITY
METAL_DETECTION
}
class TrainingType {
<<enumeration>>
HACCP
HYGIENE
FOOD_SAFETY
EQUIPMENT_OPERATION
FIRST_AID
}
class MaintenanceType {
<<enumeration>>
SCHEDULED
REPAIR
CALIBRATION
INSPECTION
}
class HoldReason {
<<enumeration>>
TEMPERATURE_DEVIATION
SAMPLE_FAILED
CONTAMINATION_SUSPECTED
PROCESS_DEVIATION
CUSTOMER_COMPLAINT
REGULATORY
}
class TemperatureStatus {
<<enumeration>>
OK
WARNING
CRITICAL
}
class InspectionResult {
<<enumeration>>
PENDING
ACCEPTED
REJECTED
CONDITIONALLY_ACCEPTED
}
Temperature --> CriticalLimit : geprüft gegen
```
### Temperature
```java
public record Temperature(BigDecimal value) {
public Temperature {
if (value.compareTo(new BigDecimal("-50")) < 0
|| value.compareTo(new BigDecimal("50")) > 0) {
throw new IllegalArgumentException(
"Temperature must be between -50°C and +50°C, got: " + value
);
}
}
public static Result<String, Temperature> of(String value) {
// Parse + Validierung
}
public boolean isWithin(CriticalLimit limit) {
return limit.contains(this.value);
}
public BigDecimal deviationFrom(CriticalLimit limit) {
// Negative = unter Min, Positive = über Max, Zero = OK
}
}
```
### CriticalLimit
```java
public record CriticalLimit(BigDecimal min, BigDecimal max) {
public CriticalLimit {
if (min != null && max != null && min.compareTo(max) >= 0) {
throw new IllegalArgumentException("min must be < max");
}
if (min == null && max == null) {
throw new IllegalArgumentException("At least one limit must be set");
}
}
public static Result<String, CriticalLimit> of(String min, String max) {
// Parse + Validierung, min/max einzeln nullable
}
public boolean contains(BigDecimal value) {
if (min != null && value.compareTo(min) < 0) return false;
if (max != null && value.compareTo(max) > 0) return false;
return true;
}
/** Warning zone: within 10% of limit boundary */
public boolean warningZoneContains(BigDecimal value) {
if (!contains(value)) return false;
BigDecimal range = max.subtract(min);
BigDecimal margin = range.multiply(new BigDecimal("0.10"));
return value.compareTo(min.add(margin)) < 0
|| value.compareTo(max.subtract(margin)) > 0;
}
}
```
### AnalysisResult
```java
public record AnalysisResult(
BigDecimal measuredValue,
String unit,
String interpretation // nullable
) {
public AnalysisResult {
Objects.requireNonNull(measuredValue, "measuredValue must not be null");
if (unit == null || unit.isBlank()) {
throw new IllegalArgumentException("unit must not be blank");
}
}
}
```
---
## Domain Errors
```java
public sealed interface TemperatureLogError {
String message();
record InvalidTemperature(String value) implements TemperatureLogError {
public String message() { return "Invalid temperature value: " + value; }
}
record InvalidCriticalLimit(String reason) implements TemperatureLogError {
public String message() { return "Invalid critical limit: " + reason; }
}
record FutureMeasurement() implements TemperatureLogError {
public String message() { return "Measurement timestamp cannot be in the future"; }
}
record InvalidMeasurementPoint(String value) implements TemperatureLogError {
public String message() { return "Unknown measurement point: " + value; }
}
}
public sealed interface CleaningPlanError {
String message();
record BlankName() implements CleaningPlanError {
public String message() { return "Cleaning plan name must not be blank"; }
}
record EmptyChecklist() implements CleaningPlanError {
public String message() { return "Checklist template must have at least one item"; }
}
record InvalidInterval(String value) implements CleaningPlanError {
public String message() { return "Unknown cleaning interval: " + value; }
}
record InvalidArea(String value) implements CleaningPlanError {
public String message() { return "Unknown cleaning area: " + value; }
}
record AlreadyActive() implements CleaningPlanError {
public String message() { return "Cleaning plan is already active"; }
}
record AlreadyInactive() implements CleaningPlanError {
public String message() { return "Cleaning plan is already inactive"; }
}
}
public sealed interface CleaningRecordError {
String message();
record NotAllItemsChecked(int checked, int total) implements CleaningRecordError {
public String message() { return "Not all items checked: " + checked + "/" + total; }
}
record AlreadyCompleted() implements CleaningRecordError {
public String message() { return "Cleaning record is already completed"; }
}
record InvalidItemPosition(int position) implements CleaningRecordError {
public String message() { return "Invalid checklist item position: " + position; }
}
record ItemAlreadyChecked(int position) implements CleaningRecordError {
public String message() { return "Checklist item already checked: " + position; }
}
}
public sealed interface InspectionError {
String message();
record CheckAlreadyRecorded(String checkType) implements InspectionError {
public String message() { return "Check already recorded: " + checkType; }
}
record ChecksIncomplete(List<String> missing) implements InspectionError {
public String message() { return "Missing checks: " + String.join(", ", missing); }
}
record AlreadyFinalized() implements InspectionError {
public String message() { return "Inspection is already finalized"; }
}
record RejectionReasonRequired() implements InspectionError {
public String message() { return "Rejection reason is required"; }
}
record ConditionsRequired() implements InspectionError {
public String message() { return "Conditions are required for conditional acceptance"; }
}
record InvalidTemperature(String reason) implements InspectionError {
public String message() { return "Invalid temperature check: " + reason; }
}
}
public sealed interface SampleRecordError {
String message();
record BlankBatchId() implements SampleRecordError {
public String message() { return "Batch ID must not be blank"; }
}
record BlankDescription() implements SampleRecordError {
public String message() { return "Sample description must not be blank"; }
}
record ResultAlreadyEntered() implements SampleRecordError {
public String message() { return "Analysis result has already been entered"; }
}
record InvalidSampleType(String value) implements SampleRecordError {
public String message() { return "Unknown sample type: " + value; }
}
record InvalidCriticalLimit(String reason) implements SampleRecordError {
public String message() { return "Invalid critical limit: " + reason; }
}
}
public sealed interface TrainingRecordError {
String message();
record InvalidDateRange() implements TrainingRecordError {
public String message() { return "ValidUntil must be after TrainingDate"; }
}
record FutureTrainingDate() implements TrainingRecordError {
public String message() { return "Training date cannot be in the future"; }
}
record BlankTrainer() implements TrainingRecordError {
public String message() { return "Trainer must not be blank"; }
}
record InvalidTrainingType(String value) implements TrainingRecordError {
public String message() { return "Unknown training type: " + value; }
}
record AlreadyRevoked() implements TrainingRecordError {
public String message() { return "Training record is already revoked"; }
}
record BlankRevokeReason() implements TrainingRecordError {
public String message() { return "Revoke reason must not be blank"; }
}
}
public sealed interface MaintenanceRecordError {
String message();
record BlankEquipmentId() implements MaintenanceRecordError {
public String message() { return "Equipment ID must not be blank"; }
}
record ScheduledDateInPast() implements MaintenanceRecordError {
public String message() { return "Scheduled date must not be in the past"; }
}
record InvalidMaintenanceType(String value) implements MaintenanceRecordError {
public String message() { return "Unknown maintenance type: " + value; }
}
record InvalidStatusTransition(String from, String to) implements MaintenanceRecordError {
public String message() { return "Cannot transition from " + from + " to " + to; }
}
record BlankFailureReason() implements MaintenanceRecordError {
public String message() { return "Failure reason must not be blank"; }
}
record BlankFindings() implements MaintenanceRecordError {
public String message() { return "Findings must not be blank for failed maintenance"; }
}
}
public sealed interface QualityHoldError {
String message();
record BlankBatchId() implements QualityHoldError {
public String message() { return "Batch ID must not be blank"; }
}
record BlankDescription() implements QualityHoldError {
public String message() { return "Hold description must not be blank"; }
}
record InvalidReason(String value) implements QualityHoldError {
public String message() { return "Unknown hold reason: " + value; }
}
record AlreadyResolved() implements QualityHoldError {
public String message() { return "Quality hold is already resolved"; }
}
record SamePersonRelease() implements QualityHoldError {
public String message() { return "Release/rejection must be by different person (Vier-Augen-Prinzip)"; }
}
record BlankJustification() implements QualityHoldError {
public String message() { return "Justification must not be blank"; }
}
record NoCorrectiveActions() implements QualityHoldError {
public String message() { return "At least one corrective action required before release"; }
}
record OpenCorrectiveActions(int count) implements QualityHoldError {
public String message() { return "Open corrective actions remaining: " + count; }
}
}
public sealed interface ProcessParameterError {
String message();
record BlankBatchId() implements ProcessParameterError {
public String message() { return "Batch ID must not be blank"; }
}
record InvalidCcpType(String value) implements ProcessParameterError {
public String message() { return "Unknown CCP type: " + value; }
}
record InvalidMeasuredValue(String reason) implements ProcessParameterError {
public String message() { return "Invalid measured value: " + reason; }
}
record InvalidCriticalLimit(String reason) implements ProcessParameterError {
public String message() { return "Invalid critical limit: " + reason; }
}
record DeviationWithoutRemarks() implements ProcessParameterError {
public String message() { return "Remarks required when measurement deviates from critical limit"; }
}
record FutureMeasurement() implements ProcessParameterError {
public String message() { return "Measurement timestamp cannot be in the future"; }
}
}
```
---
## Repository Interfaces
```java
public interface TemperatureLogRepository {
Optional<TemperatureLog> findById(TemperatureLogId id);
List<TemperatureLog> findByMeasurementPoint(MeasurementPoint point);
List<TemperatureLog> findByPeriod(Instant from, Instant to);
List<TemperatureLog> findCriticalByPeriod(Instant from, Instant to);
List<TemperatureLog> findByDeviceId(String deviceId);
void save(TemperatureLog log);
}
public interface CleaningPlanRepository {
Optional<CleaningPlan> findById(CleaningPlanId id);
List<CleaningPlan> findByStatus(CleaningPlanStatus status);
List<CleaningPlan> findByArea(CleaningArea area);
List<CleaningPlan> findActivePlans();
void save(CleaningPlan plan);
}
public interface CleaningRecordRepository {
Optional<CleaningRecord> findById(CleaningRecordId id);
List<CleaningRecord> findByCleaningPlanId(CleaningPlanId planId);
List<CleaningRecord> findByScheduledFor(LocalDate date);
List<CleaningRecord> findOverdue();
List<CleaningRecord> findByPeriod(LocalDate from, LocalDate to);
void save(CleaningRecord record);
}
public interface GoodsReceiptInspectionRepository {
Optional<GoodsReceiptInspection> findById(InspectionId id);
Optional<GoodsReceiptInspection> findByGoodsReceiptId(String goodsReceiptId);
List<GoodsReceiptInspection> findByResult(InspectionResult result);
List<GoodsReceiptInspection> findByPeriod(Instant from, Instant to);
void save(GoodsReceiptInspection inspection);
}
public interface SampleRecordRepository {
Optional<SampleRecord> findById(SampleRecordId id);
List<SampleRecord> findByBatchId(String batchId);
List<SampleRecord> findByStatus(SampleStatus status);
List<SampleRecord> findPendingResults();
List<SampleRecord> findByPeriod(Instant from, Instant to);
void save(SampleRecord record);
}
public interface TrainingRecordRepository {
Optional<TrainingRecord> findById(TrainingRecordId id);
List<TrainingRecord> findByEmployeeId(UserId employeeId);
List<TrainingRecord> findByTrainingType(TrainingType type);
List<TrainingRecord> findExpiringSoon(int withinDays);
List<TrainingRecord> findExpired();
void save(TrainingRecord record);
}
public interface MaintenanceRecordRepository {
Optional<MaintenanceRecord> findById(MaintenanceRecordId id);
List<MaintenanceRecord> findByEquipmentId(String equipmentId);
List<MaintenanceRecord> findByStatus(MaintenanceStatus status);
List<MaintenanceRecord> findOverdue();
List<MaintenanceRecord> findByPeriod(LocalDate from, LocalDate to);
void save(MaintenanceRecord record);
}
public interface QualityHoldRepository {
Optional<QualityHold> findById(QualityHoldId id);
List<QualityHold> findByBatchId(String batchId);
List<QualityHold> findByStatus(QualityHoldStatus status);
List<QualityHold> findActiveHolds();
boolean existsActiveHoldForBatch(String batchId);
void save(QualityHold hold);
}
public interface ProcessParameterRepository {
Optional<ProcessParameter> findById(ProcessParameterId id);
List<ProcessParameter> findByBatchId(String batchId);
List<ProcessParameter> findByBatchIdAndCcpType(String batchId, CcpType ccpType);
List<ProcessParameter> findDeviationsByPeriod(Instant from, Instant to);
void save(ProcessParameter parameter);
}
```
---
## Domain Services
### HaccpReportGenerator
```java
/**
* Generiert Audit-Reports aus allen Quality-Aggregates (Epic 3.8).
* Aggregiert Daten über alle 9 Aggregates für einen gegebenen Zeitraum.
*/
public class HaccpReportGenerator {
/**
* Erstellt einen umfassenden HACCP-Report für Audit-Zwecke.
* Enthält: Temperaturprotokolle, Reinigungsnachweise, WE-Kontrollen,
* Proben, Schulungen, Wartungen, Sperren, Prozessparameter.
*/
public HaccpReport generate(LocalDate from, LocalDate to);
/**
* Erstellt einen Abweichungs-Report: nur kritische Ereignisse.
*/
public DeviationReport generateDeviationReport(LocalDate from, LocalDate to);
}
public record HaccpReport(
LocalDate from,
LocalDate to,
int temperatureLogCount,
int criticalTemperatureCount,
int cleaningRecordCount,
int overdueCleaningCount,
int inspectionCount,
int rejectedInspectionCount,
int sampleCount,
int failedSampleCount,
int activeTrainingCount,
int expiredTrainingCount,
int maintenanceCount,
int failedMaintenanceCount,
int qualityHoldCount,
int processDeviationCount
) {}
```
---
## Status-Maschinen
### CleaningRecord Status
```mermaid
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> OPEN : create()
OPEN --> IN_PROGRESS : checkItem()
OPEN --> OVERDUE : markOverdue() [system]
OVERDUE --> IN_PROGRESS : checkItem()
IN_PROGRESS --> COMPLETED : complete()
OPEN : Reinigung geplant
IN_PROGRESS : Checkliste wird abgearbeitet
COMPLETED : Alle Items geprüft, completedBy gesetzt
OVERDUE : ScheduledFor überschritten
```
### GoodsReceiptInspection Status
```mermaid
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> PENDING : create()
PENDING --> ACCEPTED : finalize() [alle Checks bestanden]
PENDING --> REJECTED : reject() oder finalize() [Check failed]
PENDING --> CONDITIONALLY_ACCEPTED : acceptConditionally()
PENDING : Checks werden aufgenommen
PENDING : recordTemperatureCheck()
PENDING : recordVisualCheck()
PENDING : recordShelfLifeCheck()
PENDING : recordDocumentCheck()
ACCEPTED : Ware freigegeben
REJECTED : Ware abgelehnt, Grund dokumentiert
CONDITIONALLY_ACCEPTED : Ware mit Auflagen akzeptiert
```
### QualityHold Status
```mermaid
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> BLOCKED : block()
BLOCKED --> RELEASED : release() [Vier-Augen-Prinzip]
BLOCKED --> REJECTED : reject() [Vier-Augen-Prinzip]
BLOCKED : Charge gesperrt
BLOCKED : addCorrectiveAction()
BLOCKED : completeCorrectiveAction()
RELEASED : Charge freigegeben
RELEASED : ReleaseJustification dokumentiert
REJECTED : Charge endgültig abgelehnt
REJECTED : → Inventory write-off
```
### MaintenanceRecord Status
```mermaid
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> SCHEDULED : schedule()
SCHEDULED --> COMPLETED : complete()
SCHEDULED --> FAILED : fail()
SCHEDULED --> OVERDUE : markOverdue() [system]
SCHEDULED : Wartung geplant
COMPLETED : Durchgeführt, Befund dokumentiert
COMPLETED : nextMaintenanceDue gesetzt
FAILED : Wartung fehlgeschlagen
FAILED : failureReason + findings dokumentiert
OVERDUE : Termin überschritten
```
### SampleRecord Status
```mermaid
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
stateDiagram-v2
[*] --> PENDING : record()
PENDING --> PASSED : enterResult() [innerhalb Grenzwert]
PENDING --> FAILED : enterResult() [außerhalb Grenzwert]
PENDING : Probe entnommen, Analyse läuft
PASSED : Ergebnis innerhalb Grenzwerte
FAILED : Ergebnis außerhalb Grenzwerte
FAILED : → kann QualityHold auslösen
```
---
## Integration mit anderen BCs
```mermaid
---
config:
theme: neutral
look: classic
layout: elk
themeVariables:
background: "#f8fafc"
---
graph LR
subgraph UPSTREAM["Upstream BCs"]
PROD["Production BC\n(BatchId, BatchNumber)"]
MD["Master Data\n(ArticleId)"]
UM["User Management\n(UserId)"]
PROC["Procurement BC\n(GoodsReceiptId)"]
end
subgraph QUALITY["Quality BC"]
TL["TemperatureLog"]
CP["CleaningPlan"]
CR["CleaningRecord"]
GRI["GoodsReceiptInspection"]
SR["SampleRecord"]
TR["TrainingRecord"]
MR["MaintenanceRecord"]
QH["QualityHold"]
PP["ProcessParameter"]
end
subgraph DOWNSTREAM["Downstream BCs"]
INV["Inventory BC"]
end
PROD -->|"BatchId"| SR
PROD -->|"BatchId"| QH
PROD -->|"BatchId"| PP
UM -->|"UserId"| TL
UM -->|"UserId"| CR
UM -->|"UserId"| GRI
UM -->|"UserId"| SR
UM -->|"UserId"| TR
UM -->|"UserId"| QH
UM -->|"UserId"| PP
PROC -->|"GoodsReceiptId"| GRI
QH -->|"QualityHoldRejected\n(write-off)"| INV
```
### Upstream-Abhängigkeiten (Quality konsumiert)
| BC | Referenz | Zweck |
|---|---|---|
| **Production** | BatchId | Charge in SampleRecord, QualityHold, ProcessParameter |
| **User Management** | UserId | MeasuredBy, CompletedBy, InspectedBy, BlockedBy, etc. |
| **Procurement** | GoodsReceiptId | Verknüpfung in GoodsReceiptInspection |
| **Master Data** | ArticleId | Indirekt über BatchId (Production BC) |
### Downstream-Integrationen (Quality publiziert Events)
| Event | Konsument | Aktion |
|---|---|---|
| `QualityHoldRejected` | **Inventory BC** | Charge als Ausschuss verbuchen (write-off) |
| `QualityHoldReleased` | **Inventory BC** | Chargen-Sperre aufheben |
| `GoodsReceiptAccepted` | **Procurement BC** | Wareneingang als geprüft markieren |
| `GoodsReceiptRejected` | **Procurement BC** | Reklamation / Rücksendung auslösen |
| `TemperatureCriticalLimitExceeded` | **Notification** | Alarm an zuständige Person |
| `SampleFailed` | **Quality BC** (intern) | Kann automatisch QualityHold erzeugen |
### Abgrenzungen (gehören NICHT in Quality BC)
| Konzept | Zuständiger BC | Grund |
|---|---|---|
| SOP-Dokumente (3.7) | **Document Archive BC** | Dokument mit Versionierung, kein Quality-Aggregate |
| Allergene / Nährwerte | **Labeling BC** | Eigene Domäne mit komplexen Berechnungsregeln |
| Bestandsführung | **Inventory BC** | Stock-Operationen sind Inventory-Concern |
| Lieferantenbewertung | **Procurement BC** | Rating basiert auf WE-Inspektionen, aber gehört zum Lieferantenmanagement |
---
## Use Cases (Application Layer)
```java
// TemperatureLog
RecordTemperature → TemperatureLog.record(TemperatureLogDraft)
ListTemperatureLogs → Query (by period, measurement point)
GetCriticalTemperatures → Query (critical status only)
// CleaningPlan
CreateCleaningPlan → CleaningPlan.create(CleaningPlanDraft)
UpdateCleaningPlan → plan.update(CleaningPlanUpdateDraft)
ActivateCleaningPlan → plan.activate()
DeactivateCleaningPlan → plan.deactivate()
ListCleaningPlans → Query
// CleaningRecord
CreateCleaningRecord → CleaningRecord.create(CleaningRecordDraft)
CheckCleaningItem → record.checkItem(position, remarks)
CompleteCleaningRecord → record.complete(completedBy)
ListCleaningRecords → Query (by plan, period)
ListOverdueCleanings → Query (OVERDUE status)
// GoodsReceiptInspection
CreateInspection → GoodsReceiptInspection.create(draft)
RecordTemperatureCheck → inspection.recordTemperatureCheck(draft)
RecordVisualCheck → inspection.recordVisualCheck(draft)
RecordShelfLifeCheck → inspection.recordShelfLifeCheck(draft)
RecordDocumentCheck → inspection.recordDocumentCheck(draft)
FinalizeInspection → inspection.finalize()
GetInspection → Query
// SampleRecord
RecordSample → SampleRecord.record(SampleRecordDraft)
EnterSampleResult → sample.enterResult(AnalysisResultDraft)
ListSamplesByBatch → Query (by batchId)
ListPendingSamples → Query (PENDING status)
// TrainingRecord
RecordTraining → TrainingRecord.record(TrainingRecordDraft)
RevokeTraining → training.revoke(reason)
ListTrainingsByEmployee → Query (by employeeId)
ListExpiringTrainings → Query (expiring within N days)
// MaintenanceRecord
ScheduleMaintenance → MaintenanceRecord.schedule(draft)
CompleteMaintenance → record.complete(completionDraft)
FailMaintenance → record.fail(reason, findings)
ListOverdueMaintenance → Query (OVERDUE status)
ListMaintenanceByEquipment → Query (by equipmentId)
// QualityHold
BlockBatch → QualityHold.block(QualityHoldDraft)
AddCorrectiveAction → hold.addCorrectiveAction(draft)
CompleteCorrectiveAction → hold.completeCorrectiveAction(actionId)
ReleaseBatch → hold.release(releasedBy, justification)
RejectBatch → hold.reject(rejectedBy, justification)
ListActiveHolds → Query (BLOCKED status)
GetHoldsByBatch → Query (by batchId)
// ProcessParameter
RecordProcessParameter → ProcessParameter.record(ProcessParameterDraft)
ListParametersByBatch → Query (by batchId)
ListDeviations → Query (DEVIATION status, by period)
// HACCP Reporting (Epic 3.8)
GenerateHaccpReport → HaccpReportGenerator.generate(from, to)
GenerateDeviationReport → HaccpReportGenerator.generateDeviationReport(from, to)
```
---
## Beispiel: Wareneingangsfluss (End-to-End)
```mermaid
---
config:
theme: neutral
themeVariables:
background: "#f8fafc"
---
sequenceDiagram
participant L as Lieferant
participant M as Mitarbeiter
participant GRI as GoodsReceiptInspection
participant QH as QualityHold
participant PROC as Procurement BC
participant INV as Inventory BC
L->>M: Lieferung ankommt
M->>GRI: create(goodsReceiptId, supplierBatch, userId)
activate GRI
Note over GRI: Status: PENDING
M->>GRI: recordTemperatureCheck(3.5°C, 2-7°C)
Note over GRI: Passed ✓
M->>GRI: recordVisualCheck(intact, NORMAL, NORMAL)
Note over GRI: Passed ✓
M->>GRI: recordShelfLifeCheck(2026-04-15, 14 days)
Note over GRI: 56 Tage Restlaufzeit → Passed ✓
M->>GRI: recordDocumentCheck(true, true, true, true)
Note over GRI: Passed ✓
M->>GRI: finalize()
GRI-->>M: ACCEPTED
GRI--)PROC: GoodsReceiptAccepted Event
Note over PROC: Wareneingang als geprüft markiert
deactivate GRI
Note over M,INV: --- Alternativ: Temperatur zu hoch ---
M->>GRI: recordTemperatureCheck(12.5°C, 2-7°C)
Note over GRI: Failed ✗
M->>GRI: reject("Kühlkette unterbrochen")
GRI--)PROC: GoodsReceiptRejected Event
Note over PROC: Reklamation auslösen
```
### Code-Beispiel
```java
// 1. Wareneingang kommt an → Inspektion starten
var inspectionDraft = new GoodsReceiptInspectionDraft(
"GR-2026-02-18-001", "SB-XY-42", userId
);
var inspection = GoodsReceiptInspection.create(inspectionDraft).value();
// 2. Temperaturprüfung
inspection.recordTemperatureCheck(new TemperatureCheckDraft("3.5", "2", "7"));
// 3. Sichtkontrolle
inspection.recordVisualCheck(new VisualCheckDraft(
true, "NORMAL", "NORMAL", null
));
// 4. MHD-Prüfung
inspection.recordShelfLifeCheck(new ShelfLifeCheckDraft("2026-04-15", 14));
// 5. Dokumentenprüfung
inspection.recordDocumentCheck(new DocumentCheckDraft(true, true, true, true));
// 6. Finalisieren → ACCEPTED
inspection.finalize();
// → GoodsReceiptAccepted Event wird publiziert
// 7. Probe entnehmen (optional bei Fleischwaren)
var sampleDraft = new SampleRecordDraft(
"P-2026-02-18-001", // BatchId nach Einlagerung
"MICROBIOLOGICAL",
"Rohstoff-Fleischprobe",
"PCR",
null, "1000", // Max 1000 CFU/g
userId
);
var sample = SampleRecord.record(sampleDraft).value();
// 8. Analyseergebnis nachtragen (Laborbefund nach 2 Tagen)
sample.enterResult(new AnalysisResultDraft("250", "CFU/g", "Unauffällig"));
// → SamplePassed Event
// 9. Bei Abweichung: Charge sperren
var holdDraft = new QualityHoldDraft(
"P-2026-02-18-001",
"SAMPLE_FAILED",
"Mikrobiologische Grenzwertüberschreitung bei Probe SR-123",
userId
);
var hold = QualityHold.block(holdDraft).value();
// → QualityHoldCreated Event
// 10. Korrekturmaßnahme dokumentieren
hold.addCorrectiveAction(new CorrectiveActionDraft(
"Nachbeprobung und sensorische Prüfung",
"Max Mustermann"
));
// 11. Nach Klärung: Freigabe (durch andere Person!)
hold.release(otherUserId, "Nachprobe unauffällig, sensorisch einwandfrei");
// → QualityHoldReleased Event
```
---
## DDD Validation Checklist
- [x] Aggregate Root ist einziger Einstiegspunkt (alle 9 Aggregates)
- [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: BatchId, UserId, GoodsReceiptId, CleaningPlanId)
- [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 (9 Error-Interfaces)
- [x] Status-Maschinen explizit dokumentiert (CleaningRecord, GoodsReceiptInspection, QualityHold, MaintenanceRecord, SampleRecord)
- [x] BC-Grenzen klar definiert (SOP → Document Archive, Allergene → Labeling)
- [x] Alle Epic-3-Features abgedeckt: 3.1 Temperatur, 3.2 Reinigung, 3.3 Wareneingang, 3.4 Proben, 3.5 Schulungen, 3.6 Wartung, 3.7 SOP→Document Archive, 3.8 Reports
- [x] Domain Events für BC-Integration definiert
- [x] Vier-Augen-Prinzip bei QualityHold (BlockedBy ≠ ReleasedBy)