mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:04:49 +01:00
feat(production): Produktionsschritte zum Rezept verwalten + AuthorizationPort
Erweitert das Recipe-Aggregate um ProductionStep-Child-Entities (Add/Remove) mit vollständiger DDD-Konformität. Führt AuthorizationPort-Prüfung in allen Production Use Cases ein (analog zum usermanagement-Referenz-BC). Fixes: Request-Validierung (Size, Min/Max), Error-Code-Konsistenz, Defense-in-Depth für durationMinutes und temperatureCelsius.
This commit is contained in:
parent
c26d72fbe7
commit
cf93b847e5
27 changed files with 978 additions and 18 deletions
|
|
@ -0,0 +1,177 @@
|
|||
package de.effigenix.domain.production;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("ProductionStep Entity")
|
||||
class ProductionStepTest {
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create production step with valid inputs")
|
||||
void should_CreateStep_When_ValidInputs() {
|
||||
var draft = new ProductionStepDraft(1, "Fleisch wolfen", 15, 4);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var step = result.unsafeGetValue();
|
||||
assertThat(step.id()).isNotNull();
|
||||
assertThat(step.stepNumber()).isEqualTo(1);
|
||||
assertThat(step.description()).isEqualTo("Fleisch wolfen");
|
||||
assertThat(step.durationMinutes()).isEqualTo(15);
|
||||
assertThat(step.temperatureCelsius()).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create production step with nullable fields as null")
|
||||
void should_CreateStep_When_NullableFieldsAreNull() {
|
||||
var draft = new ProductionStepDraft(1, "Mischen", null, null);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var step = result.unsafeGetValue();
|
||||
assertThat(step.durationMinutes()).isNull();
|
||||
assertThat(step.temperatureCelsius()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when step number is 0")
|
||||
void should_Fail_When_StepNumberIsZero() {
|
||||
var draft = new ProductionStepDraft(0, "Mischen", null, null);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when step number is negative")
|
||||
void should_Fail_When_StepNumberIsNegative() {
|
||||
var draft = new ProductionStepDraft(-1, "Mischen", null, null);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when description is blank")
|
||||
void should_Fail_When_DescriptionIsBlank() {
|
||||
var draft = new ProductionStepDraft(1, "", null, null);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when description is null")
|
||||
void should_Fail_When_DescriptionIsNull() {
|
||||
var draft = new ProductionStepDraft(1, null, null, null);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when duration minutes is zero")
|
||||
void should_Fail_When_DurationMinutesIsZero() {
|
||||
var draft = new ProductionStepDraft(1, "Mischen", 0, null);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when duration minutes is negative")
|
||||
void should_Fail_When_DurationMinutesIsNegative() {
|
||||
var draft = new ProductionStepDraft(1, "Mischen", -5, null);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when temperature is below absolute zero")
|
||||
void should_Fail_When_TemperatureBelowAbsoluteZero() {
|
||||
var draft = new ProductionStepDraft(1, "Mischen", null, -274);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when temperature is above 1000")
|
||||
void should_Fail_When_TemperatureAbove1000() {
|
||||
var draft = new ProductionStepDraft(1, "Mischen", null, 1001);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should allow negative temperature within valid range")
|
||||
void should_AllowNegativeTemperature_WhenInRange() {
|
||||
var draft = new ProductionStepDraft(1, "Tiefkühlen", null, -18);
|
||||
|
||||
var result = ProductionStep.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().temperatureCelsius()).isEqualTo(-18);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Equality")
|
||||
class Equality {
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal when same ID")
|
||||
void should_BeEqual_When_SameId() {
|
||||
var step = ProductionStep.create(
|
||||
new ProductionStepDraft(1, "Mischen", 10, null)
|
||||
).unsafeGetValue();
|
||||
|
||||
var reconstituted = ProductionStep.reconstitute(
|
||||
step.id(), 2, "Other description", 20, 80
|
||||
);
|
||||
|
||||
assertThat(step).isEqualTo(reconstituted);
|
||||
assertThat(step.hashCode()).isEqualTo(reconstituted.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal when different ID")
|
||||
void should_NotBeEqual_When_DifferentId() {
|
||||
var step1 = ProductionStep.create(
|
||||
new ProductionStepDraft(1, "Mischen", null, null)
|
||||
).unsafeGetValue();
|
||||
var step2 = ProductionStep.create(
|
||||
new ProductionStepDraft(1, "Mischen", null, null)
|
||||
).unsafeGetValue();
|
||||
|
||||
assertThat(step1).isNotEqualTo(step2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ class RecipeTest {
|
|||
assertThat(recipe.outputQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
|
||||
assertThat(recipe.status()).isEqualTo(RecipeStatus.DRAFT);
|
||||
assertThat(recipe.ingredients()).isEmpty();
|
||||
assertThat(recipe.productionSteps()).isEmpty();
|
||||
assertThat(recipe.createdAt()).isNotNull();
|
||||
assertThat(recipe.updatedAt()).isNotNull();
|
||||
}
|
||||
|
|
@ -235,7 +236,7 @@ class RecipeTest {
|
|||
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||
null, new YieldPercentage(85), 14,
|
||||
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
RecipeStatus.ACTIVE, List.of(),
|
||||
RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||
);
|
||||
|
||||
|
|
@ -318,7 +319,7 @@ class RecipeTest {
|
|||
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||
null, new YieldPercentage(85), 14,
|
||||
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
RecipeStatus.ACTIVE, List.of(),
|
||||
RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||
);
|
||||
|
||||
|
|
@ -344,6 +345,129 @@ class RecipeTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("addProductionStep()")
|
||||
class AddProductionStep {
|
||||
|
||||
@Test
|
||||
@DisplayName("should add production step to DRAFT recipe")
|
||||
void should_AddStep_When_DraftStatus() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
|
||||
var result = recipe.addProductionStep(new ProductionStepDraft(1, "Fleisch wolfen", 15, 4));
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var step = result.unsafeGetValue();
|
||||
assertThat(step.id()).isNotNull();
|
||||
assertThat(step.stepNumber()).isEqualTo(1);
|
||||
assertThat(step.description()).isEqualTo("Fleisch wolfen");
|
||||
assertThat(step.durationMinutes()).isEqualTo(15);
|
||||
assertThat(step.temperatureCelsius()).isEqualTo(4);
|
||||
assertThat(recipe.productionSteps()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when recipe is ACTIVE")
|
||||
void should_Fail_When_ActiveStatus() {
|
||||
var recipe = Recipe.reconstitute(
|
||||
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||
null, new YieldPercentage(85), 14,
|
||||
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||
);
|
||||
|
||||
var result = recipe.addProductionStep(new ProductionStepDraft(1, "Mischen", null, null));
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when step number is duplicate")
|
||||
void should_Fail_When_DuplicateStepNumber() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
recipe.addProductionStep(new ProductionStepDraft(1, "Fleisch wolfen", null, null));
|
||||
|
||||
var result = recipe.addProductionStep(new ProductionStepDraft(1, "Nochmal wolfen", null, null));
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.DuplicateStepNumber.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should allow multiple steps with different numbers")
|
||||
void should_AllowMultipleSteps_WithDifferentNumbers() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
|
||||
recipe.addProductionStep(new ProductionStepDraft(1, "Wolfen", 15, null));
|
||||
recipe.addProductionStep(new ProductionStepDraft(2, "Mischen", 10, null));
|
||||
recipe.addProductionStep(new ProductionStepDraft(3, "Füllen", 20, null));
|
||||
|
||||
assertThat(recipe.productionSteps()).hasSize(3);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("removeProductionStep()")
|
||||
class RemoveProductionStep {
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove existing step by step number")
|
||||
void should_RemoveStep_When_Exists() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
recipe.addProductionStep(new ProductionStepDraft(1, "Wolfen", null, null));
|
||||
|
||||
var result = recipe.removeProductionStep(1);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(recipe.productionSteps()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when step not found")
|
||||
void should_Fail_When_StepNotFound() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
|
||||
var result = recipe.removeProductionStep(99);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.StepNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when recipe is not in DRAFT status")
|
||||
void should_Fail_When_NotDraftStatus() {
|
||||
var recipe = Recipe.reconstitute(
|
||||
RecipeId.generate(), new RecipeName("Test"), 1, RecipeType.FINISHED_PRODUCT,
|
||||
null, new YieldPercentage(85), 14,
|
||||
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||
);
|
||||
|
||||
var result = recipe.removeProductionStep(1);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should allow gaps in step numbers after removal")
|
||||
void should_AllowStepNumberGaps_AfterRemoval() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
recipe.addProductionStep(new ProductionStepDraft(1, "Wolfen", null, null));
|
||||
recipe.addProductionStep(new ProductionStepDraft(2, "Mischen", null, null));
|
||||
recipe.addProductionStep(new ProductionStepDraft(3, "Füllen", null, null));
|
||||
|
||||
recipe.removeProductionStep(2);
|
||||
|
||||
assertThat(recipe.productionSteps()).hasSize(2);
|
||||
assertThat(recipe.productionSteps().stream().map(s -> s.stepNumber()).toList())
|
||||
.containsExactly(1, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Equality")
|
||||
class Equality {
|
||||
|
|
@ -356,7 +480,7 @@ class RecipeTest {
|
|||
recipe1.id(), new RecipeName("Other"), 2, RecipeType.RAW_MATERIAL,
|
||||
null, new YieldPercentage(100), null,
|
||||
Quantity.of(new java.math.BigDecimal("50"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
RecipeStatus.ACTIVE, List.of(), recipe1.createdAt(), recipe1.updatedAt()
|
||||
RecipeStatus.ACTIVE, List.of(), List.of(), recipe1.createdAt(), recipe1.updatedAt()
|
||||
);
|
||||
|
||||
assertThat(recipe1).isEqualTo(recipe2);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue