# 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 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) - 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 checklistTemplate, String createdBy // UserId ) {} public record CleaningPlanUpdateDraft( String name, // nullable = keine Änderung String interval, // nullable List checklistTemplate // nullable ) {} ``` **Factory & Business Methods:** ```java // Factory public static Result create(CleaningPlanDraft draft); // Mutations public Result update(CleaningPlanUpdateDraft draft); public Result activate(); public Result 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 checklistItems // Kopie aus CleaningPlan-Template ) {} ``` **Factory & Business Methods:** ```java // Factory public static Result create(CleaningRecordDraft draft); // Mutations public Result checkItem(int position, String remarks); public Result complete(UserId completedBy); public Result 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 create( GoodsReceiptInspectionDraft draft ); // Einzelne Checks aufnehmen (Reihenfolge egal) public Result recordTemperatureCheck(TemperatureCheckDraft draft); public Result recordVisualCheck(VisualCheckDraft draft); public Result recordShelfLifeCheck(ShelfLifeCheckDraft draft); public Result recordDocumentCheck(DocumentCheckDraft draft); // Abschluss: leitet Result aus Einzelprüfungen ab public Result finalize(); // Manuelles Override bei CONDITIONALLY_ACCEPTED public Result acceptConditionally(String conditions); public Result reject(String reason); // Query Methods public boolean allChecksRecorded(); public List 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 record(SampleRecordDraft draft); // Ergebnis nachtragen public Result 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 record(TrainingRecordDraft draft); // Mutations public Result 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 schedule( MaintenanceRecordDraft draft ); // Status-Übergänge public Result complete(MaintenanceCompletionDraft draft); public Result fail(String failureReason, String findings); public Result 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 block(QualityHoldDraft draft); // Korrekturmaßnahmen (nur im BLOCKED-Status) public Result addCorrectiveAction(CorrectiveActionDraft draft); public Result completeCorrectiveAction(int actionId); // Status-Übergänge (terminal) public Result release(UserId releasedBy, String justification); public Result 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 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 { <> COLD_ROOM FREEZER DISPLAY_COUNTER PRODUCTION_ROOM } class CleaningArea { <> PRODUCTION_ROOM COLD_STORAGE SALES_COUNTER EQUIPMENT VEHICLE } class CleaningInterval { <> DAILY WEEKLY MONTHLY } class SampleType { <> MICROBIOLOGICAL CHEMICAL PHYSICAL SENSORY } class CcpType { <> CORE_TEMPERATURE PH_VALUE WATER_ACTIVITY METAL_DETECTION } class TrainingType { <> HACCP HYGIENE FOOD_SAFETY EQUIPMENT_OPERATION FIRST_AID } class MaintenanceType { <> SCHEDULED REPAIR CALIBRATION INSPECTION } class HoldReason { <> TEMPERATURE_DEVIATION SAMPLE_FAILED CONTAMINATION_SUSPECTED PROCESS_DEVIATION CUSTOMER_COMPLAINT REGULATORY } class TemperatureStatus { <> OK WARNING CRITICAL } class InspectionResult { <> 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 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 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 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 findById(TemperatureLogId id); List findByMeasurementPoint(MeasurementPoint point); List findByPeriod(Instant from, Instant to); List findCriticalByPeriod(Instant from, Instant to); List findByDeviceId(String deviceId); void save(TemperatureLog log); } public interface CleaningPlanRepository { Optional findById(CleaningPlanId id); List findByStatus(CleaningPlanStatus status); List findByArea(CleaningArea area); List findActivePlans(); void save(CleaningPlan plan); } public interface CleaningRecordRepository { Optional findById(CleaningRecordId id); List findByCleaningPlanId(CleaningPlanId planId); List findByScheduledFor(LocalDate date); List findOverdue(); List findByPeriod(LocalDate from, LocalDate to); void save(CleaningRecord record); } public interface GoodsReceiptInspectionRepository { Optional findById(InspectionId id); Optional findByGoodsReceiptId(String goodsReceiptId); List findByResult(InspectionResult result); List findByPeriod(Instant from, Instant to); void save(GoodsReceiptInspection inspection); } public interface SampleRecordRepository { Optional findById(SampleRecordId id); List findByBatchId(String batchId); List findByStatus(SampleStatus status); List findPendingResults(); List findByPeriod(Instant from, Instant to); void save(SampleRecord record); } public interface TrainingRecordRepository { Optional findById(TrainingRecordId id); List findByEmployeeId(UserId employeeId); List findByTrainingType(TrainingType type); List findExpiringSoon(int withinDays); List findExpired(); void save(TrainingRecord record); } public interface MaintenanceRecordRepository { Optional findById(MaintenanceRecordId id); List findByEquipmentId(String equipmentId); List findByStatus(MaintenanceStatus status); List findOverdue(); List findByPeriod(LocalDate from, LocalDate to); void save(MaintenanceRecord record); } public interface QualityHoldRepository { Optional findById(QualityHoldId id); List findByBatchId(String batchId); List findByStatus(QualityHoldStatus status); List findActiveHolds(); boolean existsActiveHoldForBatch(String batchId); void save(QualityHold hold); } public interface ProcessParameterRepository { Optional findById(ProcessParameterId id); List findByBatchId(String batchId); List findByBatchIdAndCcpType(String batchId, CcpType ccpType); List 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 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)