mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:04:49 +01:00
feat(production): Zutaten zum Rezept verwalten (#27)
Ingredient als Child Entity des Recipe Aggregates implementiert. Folgt dem Article → SalesUnit Pattern als Full Vertical Slice. Domain: IngredientId, Ingredient, IngredientDraft, Recipe.addIngredient/removeIngredient Application: AddRecipeIngredient, RemoveRecipeIngredient Use Cases Infrastructure: IngredientEntity (JPA), REST-Endpoints (POST/DELETE), Liquibase Migration Tests: RecipeTest (9 neue), IngredientTest (11 Tests)
This commit is contained in:
parent
dcaa43dc2c
commit
bee3f28b5f
22 changed files with 912 additions and 5 deletions
|
|
@ -0,0 +1,168 @@
|
|||
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("Ingredient Entity")
|
||||
class IngredientTest {
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create ingredient with valid inputs")
|
||||
void should_CreateIngredient_When_ValidInputs() {
|
||||
var draft = new IngredientDraft(1, "article-123", "5.5", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var ingredient = result.unsafeGetValue();
|
||||
assertThat(ingredient.id()).isNotNull();
|
||||
assertThat(ingredient.position()).isEqualTo(1);
|
||||
assertThat(ingredient.articleId()).isEqualTo("article-123");
|
||||
assertThat(ingredient.quantity().amount()).isEqualByComparingTo("5.5");
|
||||
assertThat(ingredient.quantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
|
||||
assertThat(ingredient.subRecipeId()).isNull();
|
||||
assertThat(ingredient.substitutable()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when position is 0")
|
||||
void should_Fail_When_PositionIsZero() {
|
||||
var draft = new IngredientDraft(0, "article-123", "5", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when position is negative")
|
||||
void should_Fail_When_PositionIsNegative() {
|
||||
var draft = new IngredientDraft(-1, "article-123", "5", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when articleId is blank")
|
||||
void should_Fail_When_ArticleIdIsBlank() {
|
||||
var draft = new IngredientDraft(1, "", "5", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when articleId is null")
|
||||
void should_Fail_When_ArticleIdIsNull() {
|
||||
var draft = new IngredientDraft(1, null, "5", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when quantity is zero")
|
||||
void should_Fail_When_QuantityIsZero() {
|
||||
var draft = new IngredientDraft(1, "article-123", "0", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when quantity is negative")
|
||||
void should_Fail_When_QuantityIsNegative() {
|
||||
var draft = new IngredientDraft(1, "article-123", "-5", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when UoM is invalid")
|
||||
void should_Fail_When_UomIsInvalid() {
|
||||
var draft = new IngredientDraft(1, "article-123", "5", "INVALID", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when quantity is not a number")
|
||||
void should_Fail_When_QuantityIsNotANumber() {
|
||||
var draft = new IngredientDraft(1, "article-123", "abc", "KILOGRAM", null, false);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create ingredient with subRecipeId")
|
||||
void should_CreateIngredient_WithSubRecipeId() {
|
||||
var draft = new IngredientDraft(1, "article-123", "10", "GRAM", "sub-recipe-456", true);
|
||||
|
||||
var result = Ingredient.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var ingredient = result.unsafeGetValue();
|
||||
assertThat(ingredient.subRecipeId()).isEqualTo("sub-recipe-456");
|
||||
assertThat(ingredient.substitutable()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Equality")
|
||||
class Equality {
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal when same ID")
|
||||
void should_BeEqual_When_SameId() {
|
||||
var ingredient = Ingredient.create(
|
||||
new IngredientDraft(1, "article-123", "5", "KILOGRAM", null, false)
|
||||
).unsafeGetValue();
|
||||
|
||||
var reconstituted = Ingredient.reconstitute(
|
||||
ingredient.id(), 2, "other-article", ingredient.quantity(), "sub", true
|
||||
);
|
||||
|
||||
assertThat(ingredient).isEqualTo(reconstituted);
|
||||
assertThat(ingredient.hashCode()).isEqualTo(reconstituted.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal when different ID")
|
||||
void should_NotBeEqual_When_DifferentId() {
|
||||
var ingredient1 = Ingredient.create(
|
||||
new IngredientDraft(1, "article-123", "5", "KILOGRAM", null, false)
|
||||
).unsafeGetValue();
|
||||
var ingredient2 = Ingredient.create(
|
||||
new IngredientDraft(1, "article-123", "5", "KILOGRAM", null, false)
|
||||
).unsafeGetValue();
|
||||
|
||||
assertThat(ingredient1).isNotEqualTo(ingredient2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import org.junit.jupiter.api.DisplayName;
|
|||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("Recipe Aggregate")
|
||||
|
|
@ -17,6 +19,10 @@ class RecipeTest {
|
|||
);
|
||||
}
|
||||
|
||||
private IngredientDraft validIngredientDraft(int position) {
|
||||
return new IngredientDraft(position, "article-123", "5.5", "KILOGRAM", null, false);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
|
@ -38,6 +44,7 @@ class RecipeTest {
|
|||
assertThat(recipe.outputQuantity().amount()).isEqualByComparingTo("100");
|
||||
assertThat(recipe.outputQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
|
||||
assertThat(recipe.status()).isEqualTo(RecipeStatus.DRAFT);
|
||||
assertThat(recipe.ingredients()).isEmpty();
|
||||
assertThat(recipe.createdAt()).isNotNull();
|
||||
assertThat(recipe.updatedAt()).isNotNull();
|
||||
}
|
||||
|
|
@ -199,6 +206,144 @@ class RecipeTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("addIngredient()")
|
||||
class AddIngredient {
|
||||
|
||||
@Test
|
||||
@DisplayName("should add ingredient to DRAFT recipe")
|
||||
void should_AddIngredient_When_DraftStatus() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
|
||||
var result = recipe.addIngredient(validIngredientDraft(1));
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var ingredient = result.unsafeGetValue();
|
||||
assertThat(ingredient.id()).isNotNull();
|
||||
assertThat(ingredient.position()).isEqualTo(1);
|
||||
assertThat(ingredient.articleId()).isEqualTo("article-123");
|
||||
assertThat(ingredient.quantity().amount()).isEqualByComparingTo("5.5");
|
||||
assertThat(ingredient.quantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
|
||||
assertThat(ingredient.substitutable()).isFalse();
|
||||
assertThat(recipe.ingredients()).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(),
|
||||
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||
);
|
||||
|
||||
var result = recipe.addIngredient(validIngredientDraft(1));
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when position is duplicate")
|
||||
void should_Fail_When_DuplicatePosition() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
recipe.addIngredient(validIngredientDraft(1));
|
||||
|
||||
var result = recipe.addIngredient(new IngredientDraft(1, "article-456", "3", "GRAM", null, false));
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.DuplicatePosition.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should allow ingredient with subRecipeId")
|
||||
void should_AllowIngredient_WithSubRecipeId() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
var draft = new IngredientDraft(1, "article-123", "10", "KILOGRAM", "sub-recipe-id", true);
|
||||
|
||||
var result = recipe.addIngredient(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var ingredient = result.unsafeGetValue();
|
||||
assertThat(ingredient.subRecipeId()).isEqualTo("sub-recipe-id");
|
||||
assertThat(ingredient.substitutable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should allow multiple ingredients with different positions")
|
||||
void should_AllowMultipleIngredients_WithDifferentPositions() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
|
||||
recipe.addIngredient(validIngredientDraft(1));
|
||||
recipe.addIngredient(validIngredientDraft(2));
|
||||
recipe.addIngredient(validIngredientDraft(3));
|
||||
|
||||
assertThat(recipe.ingredients()).hasSize(3);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("removeIngredient()")
|
||||
class RemoveIngredient {
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove existing ingredient")
|
||||
void should_RemoveIngredient_When_Exists() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
var ingredient = recipe.addIngredient(validIngredientDraft(1)).unsafeGetValue();
|
||||
|
||||
var result = recipe.removeIngredient(ingredient.id());
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(recipe.ingredients()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when ingredient not found")
|
||||
void should_Fail_When_IngredientNotFound() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
|
||||
var result = recipe.removeIngredient(IngredientId.generate());
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.IngredientNotFound.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(),
|
||||
java.time.LocalDateTime.now(), java.time.LocalDateTime.now()
|
||||
);
|
||||
|
||||
var result = recipe.removeIngredient(IngredientId.generate());
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.NotInDraftStatus.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should allow gaps in positions after removal")
|
||||
void should_AllowPositionGaps_AfterRemoval() {
|
||||
var recipe = Recipe.create(validDraft()).unsafeGetValue();
|
||||
recipe.addIngredient(validIngredientDraft(1));
|
||||
var ingredient2 = recipe.addIngredient(validIngredientDraft(2)).unsafeGetValue();
|
||||
recipe.addIngredient(validIngredientDraft(3));
|
||||
|
||||
recipe.removeIngredient(ingredient2.id());
|
||||
|
||||
assertThat(recipe.ingredients()).hasSize(2);
|
||||
assertThat(recipe.ingredients().stream().map(i -> i.position()).toList())
|
||||
.containsExactly(1, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Equality")
|
||||
class Equality {
|
||||
|
|
@ -211,7 +356,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, recipe1.createdAt(), recipe1.updatedAt()
|
||||
RecipeStatus.ACTIVE, List.of(), recipe1.createdAt(), recipe1.updatedAt()
|
||||
);
|
||||
|
||||
assertThat(recipe1).isEqualTo(recipe2);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue