mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 12:09:35 +01:00
refactor: restructure repository with separate backend and frontend directories
- Move Java backend to backend/ directory - Create frontend/ directory for TypeScript TUI and future WebUI - Update .gitignore for Node.js and worktrees - Update README.md with new repository structure - Copy documentation to backend/
This commit is contained in:
parent
ec9114aa0a
commit
c2c48a03e8
141 changed files with 734 additions and 9 deletions
|
|
@ -1,518 +0,0 @@
|
|||
# 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);
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue