1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 15:29:34 +01:00
effigenix/docs/mvp/ddd/04-production-bc.md
Sebastian Frick 4e448afa57 init
2026-02-17 08:25:06 +01:00

518 lines
14 KiB
Markdown

# 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<RecipeError, Void> addIngredient(
ArticleId articleId,
Quantity quantity,
Optional<RecipeId> nestedRecipeId
);
public Result<RecipeError, Void> removeIngredient(ArticleId articleId);
public Result<RecipeError, Void> updateYield(YieldPercentage newYield);
public Result<RecipeError, Void> addProductionStep(
String description,
Optional<Duration> duration
);
public Result<RecipeError, Void> activate();
public Result<RecipeError, Void> archive();
// Query methods
public Quantity calculateRequiredInput(Quantity desiredOutput);
public List<ArticleId> 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<BatchError, Batch> plan(
RecipeId recipeId,
Quantity plannedQuantity,
LocalDate productionDate,
LocalDate expiryDate,
UserId producedBy,
BranchId branchId
);
public Result<BatchError, Void> startProduction();
public Result<BatchError, Void> recordIngredientUsage(
ArticleId articleId,
BatchId rawMaterialBatchId,
SupplierBatchNumber supplierBatchNumber,
Quantity quantityUsed,
LocalDate expiryDate
);
public Result<BatchError, Void> complete(
Quantity actualQuantity,
Quantity waste,
Optional<String> remarks
);
public Result<BatchError, Void> cancel(String reason);
// Query methods
public boolean isTraceable(); // All ingredients have batch numbers
public List<BatchId> 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<ProductionOrderError, ProductionOrder> create(
RecipeId recipeId,
Quantity plannedQuantity,
LocalDate plannedDate,
Priority priority,
BranchId targetBranch,
UserId createdBy
);
public Result<ProductionOrderError, Void> release();
public Result<ProductionOrderError, Void> startProduction(BatchId batchId);
public Result<ProductionOrderError, Void> complete();
public Result<ProductionOrderError, Void> cancel(String reason);
public Result<ProductionOrderError, Void> 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<RecipeError, Void> validateNoCyclicDependency(
Recipe recipe,
RecipeRepository recipeRepository
);
}
```
### BatchTraceabilityService
```java
public class BatchTraceabilityService {
/**
* Finds all upstream batches (raw materials) used in a batch.
*/
public List<BatchId> findUpstreamBatches(BatchId batchId);
/**
* Finds all downstream batches (finished products) that used a raw material batch.
* CRITICAL for recalls!
*/
public List<BatchId> findDownstreamBatches(BatchId rawMaterialBatchId);
}
```
---
## Repository Interfaces
```java
package com.effigenix.domain.production;
import com.effigenix.shared.result.Result;
public interface RecipeRepository {
Result<RepositoryError, Void> save(Recipe recipe);
Result<RepositoryError, Recipe> findById(RecipeId id);
Result<RepositoryError, List<Recipe>> findActive();
Result<RepositoryError, List<Recipe>> findByArticleId(ArticleId articleId);
}
public interface BatchRepository {
Result<RepositoryError, Void> save(Batch batch);
Result<RepositoryError, Batch> findById(BatchId id);
Result<RepositoryError, List<Batch>> findByProductionDate(LocalDate date);
Result<RepositoryError, List<Batch>> findByStatus(BatchStatus status);
// For traceability
Result<RepositoryError, List<Batch>> findByUpstreamBatch(BatchId upstreamBatchId);
}
public interface ProductionOrderRepository {
Result<RepositoryError, Void> save(ProductionOrder order);
Result<RepositoryError, ProductionOrder> findById(ProductionOrderId id);
Result<RepositoryError, List<ProductionOrder>> findByPlannedDate(LocalDate date);
Result<RepositoryError, List<ProductionOrder>> 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<ApplicationError, RecipeDTO> execute(CreateRecipeCommand cmd);
}
// application/production/PlanProduction.java
public class PlanProduction {
public Result<ApplicationError, ProductionOrderDTO> execute(PlanProductionCommand cmd);
}
// application/production/StartProductionBatch.java
public class StartProductionBatch {
public Result<ApplicationError, BatchDTO> execute(StartBatchCommand cmd);
}
// application/production/CompleteProductionBatch.java
public class CompleteProductionBatch {
public Result<ApplicationError, BatchDTO> 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);
}
```