1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:20:23 +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

62 KiB

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

---
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:

/**
 * 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:

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:

// 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:

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:

/**
 * 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:

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:

// 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:

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:

/**
 * 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:

public record CleaningRecordDraft(
    String cleaningPlanId,
    String scheduledFor,         // ISO LocalDate
    List<String> checklistItems  // Kopie aus CleaningPlan-Template
) {}

Factory & Business Methods:

// 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:

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:

/**
 * 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:

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:

// 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:

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:

/**
 * 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:

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:

// 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:

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:

/**
 * 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:

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:

// 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:

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:

/**
 * 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:

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:

// 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:

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:

/**
 * 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:

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:

// 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:

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:

/**
 * 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:

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:

// Factory (einzige Erzeugung, immutable danach)
public static Result<ProcessParameterError, ProcessParameter> record(
    ProcessParameterDraft draft
);

// Query Methods
public boolean isDeviation();
public BigDecimal deviationFromLimit();

Domain Events:

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

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

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

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

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

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

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

/**
 * 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

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

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

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

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

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

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

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

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

// 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

  • Aggregate Root ist einziger Einstiegspunkt (alle 9 Aggregates)
  • Alle Änderungen gehen durch Aggregate-Root-Methoden
  • Invarianten werden in Factory und Methoden geprüft
  • Keine direkten Referenzen auf andere Aggregates (nur IDs: BatchId, UserId, GoodsReceiptId, CleaningPlanId)
  • Ein Aggregate = eine Transaktionsgrenze
  • EntityDraft-Pattern für VO-Konstruktion im Domain Layer
  • Result<E,T> für erwartbare Fehler, keine Exceptions
  • Sealed interfaces für Domain Errors (9 Error-Interfaces)
  • Status-Maschinen explizit dokumentiert (CleaningRecord, GoodsReceiptInspection, QualityHold, MaintenanceRecord, SampleRecord)
  • BC-Grenzen klar definiert (SOP → Document Archive, Allergene → Labeling)
  • 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
  • Domain Events für BC-Integration definiert
  • Vier-Augen-Prinzip bei QualityHold (BlockedBy ≠ ReleasedBy)