# Production BC - Detailliertes Domain Model **Bounded Context:** Production **Domain Type:** CORE **Verantwortung:** Rezepturverwaltung, Produktionsplanung, Chargen-Erzeugung --- ## Aggregates ### 1. Recipe (Aggregate Root) **Verantwortung:** Verwaltet mehrstufige Rezepturen mit Zutaten und Produktionsschritten. ``` Recipe (Aggregate Root) ├── RecipeId (VO) ├── Name (VO) ├── Version (VO) ├── RecipeType (VO: RAW_MATERIAL | INTERMEDIATE | FINISHED_PRODUCT) ├── YieldPercentage (VO) - Ausbeute in % ├── Ingredients[] (Entity) │ ├── Position (VO) - Reihenfolge im Rezept │ ├── ArticleId (VO) - Reference to Master Data │ ├── Quantity (VO) │ ├── RecipeId (VO) - For intermediate products (nested recipes) │ └── IsSubstitutable (VO) - Kann durch Alternativzutat ersetzt werden? ├── ProductionSteps[] (Entity) │ ├── StepNumber (VO) │ ├── Description (VO) │ ├── Duration (VO) - Geschätzte Dauer │ └── Temperature (VO) - Optional, für Räuchern/Kochen └── Status (VO: DRAFT | ACTIVE | ARCHIVED) ``` **Invarianten:** ```java /** * Recipe aggregate root. * * Invariants: * - Recipe must have at least one ingredient * - Yield percentage must be between 1-100% * - Ingredient quantities must be positive * - Nested recipes cannot create circular dependencies (A → B → A) * - Sum of ingredient quantities should roughly match expected input * - Position numbers in Ingredients must be unique * - StepNumber in ProductionSteps must be sequential (1, 2, 3, ...) */ ``` **Business Methods:** ```java public Result addIngredient( ArticleId articleId, Quantity quantity, Optional nestedRecipeId ); public Result removeIngredient(ArticleId articleId); public Result updateYield(YieldPercentage newYield); public Result addProductionStep( String description, Optional duration ); public Result activate(); public Result archive(); // Query methods public Quantity calculateRequiredInput(Quantity desiredOutput); public List getAllIngredients(); // Flattened, including nested public boolean containsNestedRecipe(); ``` **Domain Events:** ```java RecipeCreated RecipeActivated RecipeArchived IngredientAdded IngredientRemoved YieldPercentageChanged ``` --- ### 2. Batch (Aggregate Root) **Verantwortung:** Repräsentiert eine Produktions-Charge mit lückenloser Rückverfolgbarkeit. ``` Batch (Aggregate Root) ├── BatchId (VO) - Eindeutige Chargennummer (z.B. "BATCH-2026-02-17-001") ├── RecipeId (VO) - Reference to Recipe ├── ProductionDate (VO) ├── PlannedQuantity (VO) ├── ActualQuantity (VO) - Nach Produktionsrückmeldung ├── Status (VO: PLANNED | IN_PRODUCTION | COMPLETED | CANCELLED) ├── ProducedBy (VO: UserId) ├── ProducedAt (VO: BranchId) - Für Mehrfilialen ├── ExpiryDate (VO) - MHD des Endprodukts ├── UsedIngredientBatches[] (Entity) - CRITICAL for traceability! │ ├── RawMaterialBatchId (VO) - Batch of used raw material │ ├── ArticleId (VO) │ ├── SupplierBatchNumber (VO) - Von Wareneingang │ ├── QuantityUsed (VO) │ └── ExpiryDate (VO) - MHD des Rohstoffs ├── Waste (VO) - Ausschuss/Schwund └── Remarks (VO) - Optional, für Abweichungen Invariants: - Batch must reference a valid Recipe - ActualQuantity must be <= (PlannedQuantity / Recipe.YieldPercentage) - Status can only transition: PLANNED → IN_PRODUCTION → COMPLETED (or CANCELLED) - Cannot complete without recording used ingredient batches - ExpiryDate must be in the future at production time - Waste must be >= 0 - All UsedIngredientBatches must match Recipe.Ingredients ``` **Business Methods:** ```java public static Result plan( RecipeId recipeId, Quantity plannedQuantity, LocalDate productionDate, LocalDate expiryDate, UserId producedBy, BranchId branchId ); public Result startProduction(); public Result recordIngredientUsage( ArticleId articleId, BatchId rawMaterialBatchId, SupplierBatchNumber supplierBatchNumber, Quantity quantityUsed, LocalDate expiryDate ); public Result complete( Quantity actualQuantity, Quantity waste, Optional remarks ); public Result cancel(String reason); // Query methods public boolean isTraceable(); // All ingredients have batch numbers public List getUpstreamBatches(); // All used raw material batches public Quantity getYieldEfficiency(); // ActualQuantity / (PlannedQuantity / YieldPercentage) ``` **Domain Events:** ```java BatchPlanned BatchStarted IngredientUsageRecorded BatchCompleted(BatchId, ArticleId, Quantity, ExpiryDate) → triggers Inventory stock in BatchCancelled ``` --- ### 3. ProductionOrder (Aggregate Root) **Verantwortung:** Plant eine zukünftige Produktion. ``` ProductionOrder (Aggregate Root) ├── ProductionOrderId (VO) ├── RecipeId (VO) ├── PlannedQuantity (VO) ├── PlannedDate (VO) ├── Priority (VO: LOW | NORMAL | HIGH | URGENT) ├── Status (VO: PLANNED | RELEASED | IN_PRODUCTION | COMPLETED | CANCELLED) ├── CreatedBy (VO: UserId) ├── CreatedAt (VO: Timestamp) ├── TargetBranch (VO: BranchId) - For multi-branch production ├── GeneratedBatchId (VO) - Link to actual Batch when production starts └── Remarks (VO) Invariants: - Planned quantity must be positive - Planned date cannot be in the past - Can only release if materials available (checked in Application layer!) - Cannot complete without generating a Batch - Status transitions: PLANNED → RELEASED → IN_PRODUCTION → COMPLETED ``` **Business Methods:** ```java public static Result create( RecipeId recipeId, Quantity plannedQuantity, LocalDate plannedDate, Priority priority, BranchId targetBranch, UserId createdBy ); public Result release(); public Result startProduction(BatchId batchId); public Result complete(); public Result cancel(String reason); public Result reschedule(LocalDate newDate); ``` **Domain Events:** ```java ProductionOrderCreated ProductionOrderReleased → triggers Demand Planning update ProductionOrderStarted ProductionOrderCompleted ProductionOrderCancelled ProductionOrderRescheduled ``` --- ## Value Objects ### RecipeId ```java public record RecipeId(String value) { public RecipeId { if (value == null || value.isBlank()) { throw new IllegalArgumentException("RecipeId cannot be empty"); } } } ``` ### BatchId ```java public record BatchId(String value) { // Format: "BATCH-YYYY-MM-DD-XXX" public static BatchId generate(LocalDate productionDate, int sequenceNumber) { String value = String.format("BATCH-%s-%03d", productionDate, sequenceNumber); return new BatchId(value); } } ``` ### YieldPercentage ```java public record YieldPercentage(int value) { public YieldPercentage { if (value < 1 || value > 100) { throw new IllegalArgumentException( "Yield percentage must be between 1-100, got: " + value ); } } public Quantity calculateRequiredInput(Quantity desiredOutput) { // If yield is 80%, and we want 100kg output, we need 125kg input return desiredOutput.multiply(100.0 / value); } } ``` ### RecipeType ```java public enum RecipeType { RAW_MATERIAL, // Rohstoff, kein Rezept INTERMEDIATE, // Zwischenprodukt (z.B. Gewürzmischung) FINISHED_PRODUCT // Endprodukt } ``` ### ProductionOrderPriority ```java public enum Priority { LOW, NORMAL, HIGH, URGENT } ``` --- ## Domain Services ### RecipeValidator ```java public class RecipeValidator { /** * Validates that recipe does not create circular dependencies. */ public Result validateNoCyclicDependency( Recipe recipe, RecipeRepository recipeRepository ); } ``` ### BatchTraceabilityService ```java public class BatchTraceabilityService { /** * Finds all upstream batches (raw materials) used in a batch. */ public List findUpstreamBatches(BatchId batchId); /** * Finds all downstream batches (finished products) that used a raw material batch. * CRITICAL for recalls! */ public List findDownstreamBatches(BatchId rawMaterialBatchId); } ``` --- ## Repository Interfaces ```java package com.effigenix.domain.production; import com.effigenix.shared.result.Result; public interface RecipeRepository { Result save(Recipe recipe); Result findById(RecipeId id); Result> findActive(); Result> findByArticleId(ArticleId articleId); } public interface BatchRepository { Result save(Batch batch); Result findById(BatchId id); Result> findByProductionDate(LocalDate date); Result> findByStatus(BatchStatus status); // For traceability Result> findByUpstreamBatch(BatchId upstreamBatchId); } public interface ProductionOrderRepository { Result save(ProductionOrder order); Result findById(ProductionOrderId id); Result> findByPlannedDate(LocalDate date); Result> findByStatus(ProductionOrderStatus status); } ``` --- ## Domain Errors ```java public sealed interface RecipeError permits RecipeError.InvalidYieldPercentage, RecipeError.CyclicDependencyDetected, RecipeError.NoIngredientsError, RecipeError.RecipeNotFound { String message(); } public sealed interface BatchError permits BatchError.InvalidQuantity, BatchError.InvalidStatusTransition, BatchError.MissingIngredientBatches, BatchError.ExpiryDateInPast, BatchError.RecipeNotFound { String message(); } public sealed interface ProductionOrderError permits ProductionOrderError.PlannedDateInPast, ProductionOrderError.InvalidQuantity, ProductionOrderError.InvalidStatusTransition, ProductionOrderError.RecipeNotFound { String message(); } ``` --- ## Integration with other BCs ### Upstream Dependencies - **Master Data BC:** Recipe references ArticleId - **User Management BC:** Batch/ProductionOrder reference UserId - **Filiales BC:** ProductionOrder references BranchId ### Downstream Integrations - **Inventory BC:** `BatchCompleted` event triggers stock in - **Labeling BC:** Labeling reads Recipe data for nutrition calculation - **Procurement BC:** ProductionOrder triggers demand planning --- ## Use Cases (Application Layer) ```java // application/production/CreateRecipe.java public class CreateRecipe { public Result execute(CreateRecipeCommand cmd); } // application/production/PlanProduction.java public class PlanProduction { public Result execute(PlanProductionCommand cmd); } // application/production/StartProductionBatch.java public class StartProductionBatch { public Result execute(StartBatchCommand cmd); } // application/production/CompleteProductionBatch.java public class CompleteProductionBatch { public Result execute(CompleteBatchCommand cmd); // Triggers BatchCompleted event → Inventory stock in } ``` --- ## Example: Batch Creation Flow ```java // 1. Plan Production Order ProductionOrder order = ProductionOrder.create( recipeId, Quantity.of(100, "kg"), LocalDate.now().plusDays(1), Priority.NORMAL, branchId, userId ); // 2. Release Order (checks material availability in Application layer) order.release(); // 3. Start Production → Create Batch Batch batch = Batch.plan( order.recipeId(), order.plannedQuantity(), LocalDate.now(), LocalDate.now().plusDays(30), // MHD userId, branchId ); batch.startProduction(); order.startProduction(batch.id()); // 4. Record ingredient usage batch.recordIngredientUsage( ArticleId.of("ART-001"), BatchId.of("BATCH-2026-02-15-042"), // Supplier batch SupplierBatchNumber.of("SUPPLIER-12345"), Quantity.of(50, "kg"), LocalDate.now().plusDays(20) // MHD of raw material ); // 5. Complete Batch batch.complete( Quantity.of(80, "kg"), // Actual output Quantity.of(5, "kg"), // Waste Optional.of("Leichter Schwund beim Räuchern") ); order.complete(); // 6. BatchCompleted event → Inventory creates Stock entry ``` --- ## Testing Strategy ### Unit Tests (Domain Layer) ```java @Test void createBatch_withValidData_succeeds() { var result = Batch.plan(recipeId, quantity, date, expiryDate, userId, branchId); assertThat(result.isSuccess()).isTrue(); } @Test void completeBatch_withoutIngredients_fails() { var batch = Batch.plan(...).unsafeGetValue(); batch.startProduction(); var result = batch.complete(quantity, waste, Optional.empty()); assertThat(result.isFailure()).isTrue(); // Should fail with MissingIngredientBatches error } ``` ### Integration Tests (Application Layer) ```java @Test void completeProductionBatch_updatesInventory() { // Given: Production order and started batch // When: Complete batch var result = completeProductionBatch.execute(command); // Then: Inventory stock should increase var stock = inventoryRepository.findByArticle(articleId); assertThat(stock.quantity()).isEqualTo(expectedQuantity); } ```