1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 17:19:56 +01:00

feat(production): Recipe Aggregate als Full Vertical Slice (#26)

Domain: Recipe Aggregate Root mit create(RecipeDraft), RecipeId, RecipeName,
RecipeType, RecipeStatus, RecipeError, RecipeRepository Interface.
Application: CreateRecipe Use Case mit Name+Version Uniqueness-Check.
Infrastructure: JPA Entity/Mapper/Repository, REST POST /api/recipes,
Liquibase Migration, ProductionErrorHttpStatusMapper, Spring Config.
Tests: 15 Unit Tests für Recipe Aggregate (75 total im Production BC).
This commit is contained in:
Sebastian Frick 2026-02-19 10:12:04 +01:00
parent 24a6869faf
commit 9b9b7311d1
23 changed files with 1095 additions and 0 deletions

View file

@ -0,0 +1,230 @@
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("Recipe Aggregate")
class RecipeTest {
private RecipeDraft validDraft() {
return new RecipeDraft(
"Bratwurst Grob", 1, RecipeType.FINISHED_PRODUCT,
"Grobe Bratwurst nach Hausrezept", 85,
14, "100", "KILOGRAM"
);
}
@Nested
@DisplayName("create()")
class Create {
@Test
@DisplayName("should create recipe in DRAFT status with valid inputs")
void should_CreateInDraftStatus_When_ValidInputs() {
var result = Recipe.create(validDraft());
assertThat(result.isSuccess()).isTrue();
var recipe = result.unsafeGetValue();
assertThat(recipe.id()).isNotNull();
assertThat(recipe.name().value()).isEqualTo("Bratwurst Grob");
assertThat(recipe.version()).isEqualTo(1);
assertThat(recipe.type()).isEqualTo(RecipeType.FINISHED_PRODUCT);
assertThat(recipe.description()).isEqualTo("Grobe Bratwurst nach Hausrezept");
assertThat(recipe.yieldPercentage().value()).isEqualTo(85);
assertThat(recipe.shelfLifeDays()).isEqualTo(14);
assertThat(recipe.outputQuantity().amount()).isEqualByComparingTo("100");
assertThat(recipe.outputQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
assertThat(recipe.status()).isEqualTo(RecipeStatus.DRAFT);
assertThat(recipe.createdAt()).isNotNull();
assertThat(recipe.updatedAt()).isNotNull();
}
@Test
@DisplayName("should fail when name is blank")
void should_Fail_When_NameIsBlank() {
var draft = new RecipeDraft(
"", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name is null")
void should_Fail_When_NameIsNull() {
var draft = new RecipeDraft(
null, 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when version is less than 1")
void should_Fail_When_VersionLessThanOne() {
var draft = new RecipeDraft(
"Test", 0, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when yield percentage is 0")
void should_Fail_When_YieldPercentageIsZero() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 0, 14, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when yield percentage is 201")
void should_Fail_When_YieldPercentageIs201() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 201, 14, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when shelf life is 0 for FINISHED_PRODUCT")
void should_Fail_When_ShelfLifeZeroForFinishedProduct() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 0, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidShelfLife.class);
}
@Test
@DisplayName("should fail when shelf life is null for FINISHED_PRODUCT")
void should_Fail_When_ShelfLifeNullForFinishedProduct() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, null, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidShelfLife.class);
}
@Test
@DisplayName("should fail when shelf life is 0 for INTERMEDIATE")
void should_Fail_When_ShelfLifeZeroForIntermediate() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.INTERMEDIATE, null, 85, 0, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.InvalidShelfLife.class);
}
@Test
@DisplayName("should allow null shelf life for RAW_MATERIAL")
void should_AllowNullShelfLife_When_RawMaterial() {
var draft = new RecipeDraft(
"Schweinefleisch", 1, RecipeType.RAW_MATERIAL, null, 100, null, "50", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().shelfLifeDays()).isNull();
}
@Test
@DisplayName("should fail when output quantity is zero")
void should_Fail_When_OutputQuantityZero() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "0", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when output UoM is invalid")
void should_Fail_When_OutputUomInvalid() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "INVALID"
);
var result = Recipe.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.ValidationFailure.class);
}
@Test
@DisplayName("should allow description to be null")
void should_AllowNullDescription() {
var draft = new RecipeDraft(
"Test", 1, RecipeType.FINISHED_PRODUCT, null, 85, 14, "100", "KILOGRAM"
);
var result = Recipe.create(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().description()).isNull();
}
}
@Nested
@DisplayName("Equality")
class Equality {
@Test
@DisplayName("should be equal when same ID")
void should_BeEqual_When_SameId() {
var recipe1 = Recipe.create(validDraft()).unsafeGetValue();
var recipe2 = Recipe.reconstitute(
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()
);
assertThat(recipe1).isEqualTo(recipe2);
assertThat(recipe1.hashCode()).isEqualTo(recipe2.hashCode());
}
@Test
@DisplayName("should not be equal when different ID")
void should_NotBeEqual_When_DifferentId() {
var recipe1 = Recipe.create(validDraft()).unsafeGetValue();
var recipe2 = Recipe.create(validDraft()).unsafeGetValue();
assertThat(recipe1).isNotEqualTo(recipe2);
}
}
}