mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 19:40:18 +01:00
- 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)
1849 lines
62 KiB
Markdown
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)
|