mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:39:57 +01:00
feat(production): Produktionsauftrag anlegen (US-P13, #38)
ProductionOrder-Aggregate mit DDD + Clean Architecture eingeführt. Produktionsleiter können Aufträge mit Rezept, Menge, Termin und Priorität planen. Validierung: Quantity > 0, PlannedDate nicht in Vergangenheit, Priority (LOW/NORMAL/HIGH/URGENT), Recipe ACTIVE.
This commit is contained in:
parent
5020df5d93
commit
ba37ff647b
26 changed files with 1781 additions and 0 deletions
|
|
@ -0,0 +1,236 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
import de.effigenix.application.production.command.CreateProductionOrderCommand;
|
||||
import de.effigenix.domain.production.*;
|
||||
import de.effigenix.shared.common.Quantity;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.common.UnitOfMeasure;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("CreateProductionOrder Use Case")
|
||||
class CreateProductionOrderTest {
|
||||
|
||||
@Mock private ProductionOrderRepository productionOrderRepository;
|
||||
@Mock private RecipeRepository recipeRepository;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private CreateProductionOrder createProductionOrder;
|
||||
private ActorId performedBy;
|
||||
|
||||
private static final LocalDate PLANNED_DATE = LocalDate.now().plusDays(7);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
createProductionOrder = new CreateProductionOrder(productionOrderRepository, recipeRepository, authPort);
|
||||
performedBy = ActorId.of("admin-user");
|
||||
}
|
||||
|
||||
private CreateProductionOrderCommand validCommand() {
|
||||
return new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
}
|
||||
|
||||
private Recipe activeRecipe() {
|
||||
return Recipe.reconstitute(
|
||||
RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
|
||||
null, new YieldPercentage(85), 14,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
"article-123", RecipeStatus.ACTIVE, List.of(), List.of(),
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
private Recipe draftRecipe() {
|
||||
return Recipe.reconstitute(
|
||||
RecipeId.of("recipe-1"), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
|
||||
null, new YieldPercentage(85), 14,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
"article-123", RecipeStatus.DRAFT, List.of(), List.of(),
|
||||
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create production order when recipe is ACTIVE and all data valid")
|
||||
void should_CreateOrder_When_ValidCommand() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(activeRecipe())));
|
||||
when(productionOrderRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = createProductionOrder.execute(validCommand(), performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var order = result.unsafeGetValue();
|
||||
assertThat(order.status()).isEqualTo(ProductionOrderStatus.PLANNED);
|
||||
assertThat(order.recipeId().value()).isEqualTo("recipe-1");
|
||||
assertThat(order.priority()).isEqualTo(Priority.NORMAL);
|
||||
verify(productionOrderRepository).save(any(ProductionOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when actor lacks PRODUCTION_ORDER_WRITE permission")
|
||||
void should_Fail_When_Unauthorized() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(false);
|
||||
|
||||
var result = createProductionOrder.execute(validCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.Unauthorized.class);
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when recipe not found")
|
||||
void should_Fail_When_RecipeNotFound() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = createProductionOrder.execute(validCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class);
|
||||
assertThat(result.unsafeGetError().message()).contains("recipe-1");
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when recipe is not ACTIVE")
|
||||
void should_Fail_When_RecipeNotActive() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(draftRecipe())));
|
||||
|
||||
var result = createProductionOrder.execute(validCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RecipeNotActive.class);
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when recipe repository returns error")
|
||||
void should_Fail_When_RecipeRepositoryError() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = createProductionOrder.execute(validCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class);
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when production order repository save fails")
|
||||
void should_Fail_When_SaveFails() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(activeRecipe())));
|
||||
when(productionOrderRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var result = createProductionOrder.execute(validCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when planned quantity is invalid")
|
||||
void should_Fail_When_InvalidQuantity() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(activeRecipe())));
|
||||
|
||||
var cmd = new CreateProductionOrderCommand("recipe-1", "0", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
var result = createProductionOrder.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when planned date is in the past")
|
||||
void should_Fail_When_PlannedDateInPast() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(activeRecipe())));
|
||||
|
||||
var cmd = new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM",
|
||||
LocalDate.now().minusDays(1), "NORMAL", null);
|
||||
var result = createProductionOrder.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.PlannedDateInPast.class);
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when priority is invalid")
|
||||
void should_Fail_When_InvalidPriority() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(activeRecipe())));
|
||||
|
||||
var cmd = new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM", PLANNED_DATE, "SUPER_HIGH", null);
|
||||
var result = createProductionOrder.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPriority.class);
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when unit is invalid")
|
||||
void should_Fail_When_InvalidUnit() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(activeRecipe())));
|
||||
|
||||
var cmd = new CreateProductionOrderCommand("recipe-1", "100", "INVALID", PLANNED_DATE, "NORMAL", null);
|
||||
var result = createProductionOrder.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
verify(productionOrderRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should pass notes through to production order")
|
||||
void should_PassNotes_When_Provided() {
|
||||
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true);
|
||||
when(recipeRepository.findById(RecipeId.of("recipe-1")))
|
||||
.thenReturn(Result.success(Optional.of(activeRecipe())));
|
||||
when(productionOrderRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var cmd = new CreateProductionOrderCommand("recipe-1", "100", "KILOGRAM", PLANNED_DATE, "URGENT", "Eilauftrag");
|
||||
var result = createProductionOrder.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().notes()).isEqualTo("Eilauftrag");
|
||||
assertThat(result.unsafeGetValue().priority()).isEqualTo(Priority.URGENT);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
package de.effigenix.domain.production;
|
||||
|
||||
import de.effigenix.shared.common.Quantity;
|
||||
import de.effigenix.shared.common.UnitOfMeasure;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("ProductionOrder Aggregate")
|
||||
class ProductionOrderTest {
|
||||
|
||||
private static final LocalDate FUTURE_DATE = LocalDate.now().plusDays(7);
|
||||
|
||||
private ProductionOrderDraft validDraft() {
|
||||
return new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "NORMAL", null);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create production order with valid draft")
|
||||
void should_CreateOrder_When_ValidDraft() {
|
||||
var result = ProductionOrder.create(validDraft());
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var order = result.unsafeGetValue();
|
||||
assertThat(order.id()).isNotNull();
|
||||
assertThat(order.recipeId().value()).isEqualTo("recipe-123");
|
||||
assertThat(order.status()).isEqualTo(ProductionOrderStatus.PLANNED);
|
||||
assertThat(order.plannedQuantity().amount()).isEqualByComparingTo(new BigDecimal("100"));
|
||||
assertThat(order.plannedQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM);
|
||||
assertThat(order.plannedDate()).isEqualTo(FUTURE_DATE);
|
||||
assertThat(order.priority()).isEqualTo(Priority.NORMAL);
|
||||
assertThat(order.notes()).isNull();
|
||||
assertThat(order.createdAt()).isNotNull();
|
||||
assertThat(order.updatedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept notes when provided")
|
||||
void should_AcceptNotes_When_Provided() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "HIGH", "Eilauftrag");
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().notes()).isEqualTo("Eilauftrag");
|
||||
assertThat(result.unsafeGetValue().priority()).isEqualTo(Priority.HIGH);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept all priority values")
|
||||
void should_AcceptAllPriorities() {
|
||||
for (Priority p : Priority.values()) {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, p.name(), null);
|
||||
var result = ProductionOrder.create(draft);
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().priority()).isEqualTo(p);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when recipeId is blank")
|
||||
void should_Fail_When_RecipeIdBlank() {
|
||||
var draft = new ProductionOrderDraft("", "100", "KILOGRAM", FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when recipeId is null")
|
||||
void should_Fail_When_RecipeIdNull() {
|
||||
var draft = new ProductionOrderDraft(null, "100", "KILOGRAM", FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedQuantity is zero")
|
||||
void should_Fail_When_QuantityZero() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "0", "KILOGRAM", FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedQuantity is negative")
|
||||
void should_Fail_When_QuantityNegative() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "-5", "KILOGRAM", FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedQuantity is not a number")
|
||||
void should_Fail_When_QuantityNotANumber() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "abc", "KILOGRAM", FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when unit is invalid")
|
||||
void should_Fail_When_UnitInvalid() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "INVALID_UNIT", FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedDate is yesterday")
|
||||
void should_Fail_When_PlannedDateYesterday() {
|
||||
var yesterday = LocalDate.now().minusDays(1);
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", yesterday, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.PlannedDateInPast.class);
|
||||
var err = (ProductionOrderError.PlannedDateInPast) result.unsafeGetError();
|
||||
assertThat(err.date()).isEqualTo(yesterday);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should succeed when plannedDate is today")
|
||||
void should_Succeed_When_PlannedDateToday() {
|
||||
var today = LocalDate.now();
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", today, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().plannedDate()).isEqualTo(today);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedDate is null")
|
||||
void should_Fail_When_PlannedDateNull() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", null, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when priority is invalid")
|
||||
void should_Fail_When_PriorityInvalid() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "SUPER_HIGH", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPriority.class);
|
||||
var err = (ProductionOrderError.InvalidPriority) result.unsafeGetError();
|
||||
assertThat(err.value()).isEqualTo("SUPER_HIGH");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when priority is null")
|
||||
void should_Fail_When_PriorityNull() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, null, null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPriority.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept decimal quantity")
|
||||
void should_Accept_When_DecimalQuantity() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "50.75", "LITER", FUTURE_DATE, "LOW", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().plannedQuantity().amount())
|
||||
.isEqualByComparingTo(new BigDecimal("50.75"));
|
||||
assertThat(result.unsafeGetValue().plannedQuantity().uom()).isEqualTo(UnitOfMeasure.LITER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedQuantity is null")
|
||||
void should_Fail_When_QuantityNull() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", null, "KILOGRAM", FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedQuantityUnit is null")
|
||||
void should_Fail_When_UnitNull() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", null, FUTURE_DATE, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPlannedQuantity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when plannedDate is far in the past")
|
||||
void should_Fail_When_PlannedDateFarInPast() {
|
||||
var pastDate = LocalDate.of(2020, 1, 1);
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", pastDate, "NORMAL", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.PlannedDateInPast.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when priority is empty string")
|
||||
void should_Fail_When_PriorityEmpty() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPriority.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when priority is lowercase")
|
||||
void should_Fail_When_PriorityLowercase() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "normal", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.InvalidPriority.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should set version to 0 on creation")
|
||||
void should_SetVersionToZero_When_Created() {
|
||||
var result = ProductionOrder.create(validDraft());
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().version()).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should set createdAt equal to updatedAt on creation")
|
||||
void should_SetTimestampsEqual_When_Created() {
|
||||
var result = ProductionOrder.create(validDraft());
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var order = result.unsafeGetValue();
|
||||
assertThat(order.createdAt()).isEqualTo(order.updatedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept null notes")
|
||||
void should_Accept_When_NotesNull() {
|
||||
var draft = new ProductionOrderDraft("recipe-123", "100", "KILOGRAM", FUTURE_DATE, "URGENT", null);
|
||||
|
||||
var result = ProductionOrder.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().notes()).isNull();
|
||||
assertThat(result.unsafeGetValue().priority()).isEqualTo(Priority.URGENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("reconstitute()")
|
||||
class Reconstitute {
|
||||
|
||||
@Test
|
||||
@DisplayName("should reconstitute production order from persistence")
|
||||
void should_Reconstitute_When_ValidData() {
|
||||
var order = ProductionOrder.reconstitute(
|
||||
ProductionOrderId.of("order-1"),
|
||||
RecipeId.of("recipe-123"),
|
||||
ProductionOrderStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
FUTURE_DATE,
|
||||
Priority.HIGH,
|
||||
"Wichtiger Auftrag",
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
5L
|
||||
);
|
||||
|
||||
assertThat(order.id().value()).isEqualTo("order-1");
|
||||
assertThat(order.recipeId().value()).isEqualTo("recipe-123");
|
||||
assertThat(order.status()).isEqualTo(ProductionOrderStatus.PLANNED);
|
||||
assertThat(order.priority()).isEqualTo(Priority.HIGH);
|
||||
assertThat(order.notes()).isEqualTo("Wichtiger Auftrag");
|
||||
assertThat(order.version()).isEqualTo(5L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
package de.effigenix.infrastructure.production.web;
|
||||
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.infrastructure.AbstractIntegrationTest;
|
||||
import de.effigenix.infrastructure.production.web.dto.CreateProductionOrderRequest;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
|
||||
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@DisplayName("ProductionOrder Controller Integration Tests")
|
||||
class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
|
||||
private static final LocalDate PLANNED_DATE = LocalDate.now().plusDays(7);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
RoleEntity adminRole = createRole(RoleName.ADMIN, "Admin");
|
||||
RoleEntity viewerRole = createRole(RoleName.PRODUCTION_WORKER, "Viewer");
|
||||
|
||||
UserEntity admin = createUser("po.admin", "po.admin@test.com", Set.of(adminRole), "BRANCH-01");
|
||||
UserEntity viewer = createUser("po.viewer", "po.viewer@test.com", Set.of(viewerRole), "BRANCH-01");
|
||||
|
||||
adminToken = generateToken(admin.getId(), "po.admin",
|
||||
"PRODUCTION_ORDER_WRITE,PRODUCTION_ORDER_READ,RECIPE_WRITE,RECIPE_READ");
|
||||
viewerToken = generateToken(viewer.getId(), "po.viewer", "USER_READ");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /api/production/production-orders – Produktionsauftrag anlegen")
|
||||
class CreateProductionOrderEndpoint {
|
||||
|
||||
@Test
|
||||
@DisplayName("Produktionsauftrag mit gültigen Daten → 201")
|
||||
void createOrder_withValidData_returns201() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.recipeId").value(recipeId))
|
||||
.andExpect(jsonPath("$.status").value("PLANNED"))
|
||||
.andExpect(jsonPath("$.plannedQuantity").value("100.000000"))
|
||||
.andExpect(jsonPath("$.plannedQuantityUnit").value("KILOGRAM"))
|
||||
.andExpect(jsonPath("$.plannedDate").value(PLANNED_DATE.toString()))
|
||||
.andExpect(jsonPath("$.priority").value("NORMAL"))
|
||||
.andExpect(jsonPath("$.notes").doesNotExist())
|
||||
.andExpect(jsonPath("$.createdAt").isNotEmpty())
|
||||
.andExpect(jsonPath("$.updatedAt").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Produktionsauftrag mit Notes → 201")
|
||||
void createOrder_withNotes_returns201() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "50", "LITER", PLANNED_DATE, "URGENT", "Eilauftrag Kunde X");
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.priority").value("URGENT"))
|
||||
.andExpect(jsonPath("$.notes").value("Eilauftrag Kunde X"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PlannedQuantity 0 → 400")
|
||||
void createOrder_zeroQuantity_returns400() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "0", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PLANNED_QUANTITY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Negative Menge → 400")
|
||||
void createOrder_negativeQuantity_returns400() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "-10", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PLANNED_QUANTITY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PlannedDate in der Vergangenheit → 400")
|
||||
void createOrder_pastDate_returns400() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "100", "KILOGRAM", LocalDate.now().minusDays(1), "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_PLANNED_DATE_IN_PAST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ungültige Priority → 400")
|
||||
void createOrder_invalidPriority_returns400() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "100", "KILOGRAM", PLANNED_DATE, "SUPER_HIGH", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PRIORITY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Rezept nicht gefunden → 400")
|
||||
void createOrder_recipeNotFound_returns400() throws Exception {
|
||||
var request = new CreateProductionOrderRequest(
|
||||
UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Rezept nicht ACTIVE (DRAFT) → 409")
|
||||
void createOrder_recipeNotActive_returns409() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_RECIPE_NOT_ACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("recipeId leer → 400 (Bean Validation)")
|
||||
void createOrder_blankRecipeId_returns400() throws Exception {
|
||||
var request = new CreateProductionOrderRequest(
|
||||
"", "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ungültige Unit → 400")
|
||||
void createOrder_invalidUnit_returns400() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "100", "INVALID_UNIT", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PLANNED_QUANTITY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PlannedDate heute → 201 (Grenzwert)")
|
||||
void createOrder_todayDate_returns201() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "100", "KILOGRAM", LocalDate.now(), "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.plannedDate").value(LocalDate.now().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("priority leer → 400 (Bean Validation)")
|
||||
void createOrder_blankPriority_returns400() throws Exception {
|
||||
var request = new CreateProductionOrderRequest(
|
||||
UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("plannedDate null → 400 (Bean Validation)")
|
||||
void createOrder_nullDate_returns400() throws Exception {
|
||||
String json = """
|
||||
{"recipeId": "%s", "plannedQuantity": "100", "plannedQuantityUnit": "KILOGRAM", "priority": "NORMAL"}
|
||||
""".formatted(UUID.randomUUID().toString());
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Alle Priority-Werte → 201")
|
||||
void createOrder_allPriorities_returns201() throws Exception {
|
||||
for (String priority : new String[]{"LOW", "NORMAL", "HIGH", "URGENT"}) {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
var request = new CreateProductionOrderRequest(
|
||||
recipeId, "100", "KILOGRAM", PLANNED_DATE, priority, null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.priority").value(priority));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("plannedQuantity leer → 400 (Bean Validation)")
|
||||
void createOrder_blankQuantity_returns400() throws Exception {
|
||||
var request = new CreateProductionOrderRequest(
|
||||
UUID.randomUUID().toString(), "", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Authorization")
|
||||
class AuthTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Ohne PRODUCTION_ORDER_WRITE → 403")
|
||||
void createOrder_withViewerToken_returns403() throws Exception {
|
||||
var request = new CreateProductionOrderRequest(
|
||||
UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ohne Token → 401")
|
||||
void createOrder_withoutToken_returns401() throws Exception {
|
||||
var request = new CreateProductionOrderRequest(
|
||||
UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);
|
||||
|
||||
mockMvc.perform(post("/api/production/production-orders")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createActiveRecipe() throws Exception {
|
||||
String recipeId = createDraftRecipe();
|
||||
|
||||
// Add ingredient (required for activation)
|
||||
String ingredientJson = """
|
||||
{"position": 1, "articleId": "%s", "quantity": "5.5", "uom": "KILOGRAM", "substitutable": false}
|
||||
""".formatted(UUID.randomUUID().toString());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/ingredients", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(ingredientJson))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
// Activate
|
||||
mockMvc.perform(post("/api/recipes/{id}/activate", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
return recipeId;
|
||||
}
|
||||
|
||||
private String createDraftRecipe() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"name": "Test-Rezept-%s",
|
||||
"version": 1,
|
||||
"type": "FINISHED_PRODUCT",
|
||||
"description": "Testrezept",
|
||||
"yieldPercentage": 85,
|
||||
"shelfLifeDays": 14,
|
||||
"outputQuantity": "100",
|
||||
"outputUom": "KILOGRAM",
|
||||
"articleId": "article-123"
|
||||
}
|
||||
""".formatted(UUID.randomUUID().toString().substring(0, 8));
|
||||
|
||||
var result = mockMvc.perform(post("/api/recipes")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue