mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 13:49:36 +01:00
feat(production): Chargen abfragen und suchen (GetBatch, ListBatches, FindBatchByNumber)
Full Vertical Slice für Batch-Lese-Endpoints:
- GET /api/production/batches/{id}
- GET /api/production/batches (Filter: status, productionDate, articleId)
- GET /api/production/batches/by-number/{batchNumber}
articleId-Filter löst über RecipeRepository.findByArticleId() die
zugehörigen Recipes auf und sucht dann Batches per findByRecipeIds().
Closes #34
This commit is contained in:
parent
fef3baa0ae
commit
1c65ac7795
20 changed files with 1348 additions and 1 deletions
|
|
@ -0,0 +1,36 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
import de.effigenix.domain.production.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public class FindBatchByNumber {
|
||||
|
||||
private final BatchRepository batchRepository;
|
||||
private final AuthorizationPort authorizationPort;
|
||||
|
||||
public FindBatchByNumber(BatchRepository batchRepository, AuthorizationPort authorizationPort) {
|
||||
this.batchRepository = batchRepository;
|
||||
this.authorizationPort = authorizationPort;
|
||||
}
|
||||
|
||||
public Result<BatchError, Batch> execute(BatchNumber batchNumber, ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
switch (batchRepository.findByBatchNumber(batchNumber)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var opt) -> {
|
||||
if (opt.isEmpty()) {
|
||||
return Result.failure(new BatchError.BatchNotFoundByNumber(batchNumber));
|
||||
}
|
||||
return Result.success(opt.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
import de.effigenix.domain.production.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public class GetBatch {
|
||||
|
||||
private final BatchRepository batchRepository;
|
||||
private final AuthorizationPort authorizationPort;
|
||||
|
||||
public GetBatch(BatchRepository batchRepository, AuthorizationPort authorizationPort) {
|
||||
this.batchRepository = batchRepository;
|
||||
this.authorizationPort = authorizationPort;
|
||||
}
|
||||
|
||||
public Result<BatchError, Batch> execute(BatchId batchId, ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
switch (batchRepository.findById(batchId)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var opt) -> {
|
||||
if (opt.isEmpty()) {
|
||||
return Result.failure(new BatchError.BatchNotFound(batchId));
|
||||
}
|
||||
return Result.success(opt.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
import de.effigenix.domain.production.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public class ListBatches {
|
||||
|
||||
private final BatchRepository batchRepository;
|
||||
private final RecipeRepository recipeRepository;
|
||||
private final AuthorizationPort authorizationPort;
|
||||
|
||||
public ListBatches(BatchRepository batchRepository, RecipeRepository recipeRepository,
|
||||
AuthorizationPort authorizationPort) {
|
||||
this.batchRepository = batchRepository;
|
||||
this.recipeRepository = recipeRepository;
|
||||
this.authorizationPort = authorizationPort;
|
||||
}
|
||||
|
||||
public Result<BatchError, List<Batch>> execute(ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
switch (batchRepository.findAll()) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var batches) ->
|
||||
{ return Result.success(batches); }
|
||||
}
|
||||
}
|
||||
|
||||
public Result<BatchError, List<Batch>> executeByStatus(BatchStatus status, ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
switch (batchRepository.findByStatus(status)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var batches) ->
|
||||
{ return Result.success(batches); }
|
||||
}
|
||||
}
|
||||
|
||||
public Result<BatchError, List<Batch>> executeByProductionDate(LocalDate date, ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
switch (batchRepository.findByProductionDate(date)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var batches) ->
|
||||
{ return Result.success(batches); }
|
||||
}
|
||||
}
|
||||
|
||||
public Result<BatchError, List<Batch>> executeByArticleId(String articleId, ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
switch (recipeRepository.findByArticleId(articleId)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var recipes) -> {
|
||||
if (recipes.isEmpty()) {
|
||||
return Result.success(List.of());
|
||||
}
|
||||
List<RecipeId> recipeIds = recipes.stream().map(Recipe::id).toList();
|
||||
switch (batchRepository.findByRecipeIds(recipeIds)) {
|
||||
case Result.Failure(var batchErr) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(batchErr.message())); }
|
||||
case Result.Success(var batches) ->
|
||||
{ return Result.success(batches); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,11 @@ public sealed interface BatchError {
|
|||
@Override public String message() { return "Recipe '" + recipeId.value() + "' is not in ACTIVE status"; }
|
||||
}
|
||||
|
||||
record BatchNotFoundByNumber(BatchNumber batchNumber) implements BatchError {
|
||||
@Override public String code() { return "BATCH_NOT_FOUND_BY_NUMBER"; }
|
||||
@Override public String message() { return "Batch with number '" + batchNumber.value() + "' not found"; }
|
||||
}
|
||||
|
||||
record ValidationFailure(String message) implements BatchError {
|
||||
@Override public String code() { return "BATCH_VALIDATION_ERROR"; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package de.effigenix.domain.production;
|
|||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
|
@ -12,5 +13,13 @@ public interface BatchRepository {
|
|||
|
||||
Result<RepositoryError, List<Batch>> findAll();
|
||||
|
||||
Result<RepositoryError, Optional<Batch>> findByBatchNumber(BatchNumber batchNumber);
|
||||
|
||||
Result<RepositoryError, List<Batch>> findByStatus(BatchStatus status);
|
||||
|
||||
Result<RepositoryError, List<Batch>> findByProductionDate(LocalDate date);
|
||||
|
||||
Result<RepositoryError, List<Batch>> findByRecipeIds(List<RecipeId> recipeIds);
|
||||
|
||||
Result<RepositoryError, Void> save(Batch batch);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,6 @@ public interface RecipeRepository {
|
|||
Result<RepositoryError, Boolean> existsByNameAndVersion(String name, int version);
|
||||
|
||||
Result<RepositoryError, List<Recipe>> findByStatus(RecipeStatus status);
|
||||
|
||||
Result<RepositoryError, List<Recipe>> findByArticleId(String articleId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import de.effigenix.application.production.ArchiveRecipe;
|
|||
import de.effigenix.application.production.AddProductionStep;
|
||||
import de.effigenix.application.production.AddRecipeIngredient;
|
||||
import de.effigenix.application.production.CreateRecipe;
|
||||
import de.effigenix.application.production.FindBatchByNumber;
|
||||
import de.effigenix.application.production.GetBatch;
|
||||
import de.effigenix.application.production.ListBatches;
|
||||
import de.effigenix.application.production.PlanBatch;
|
||||
import de.effigenix.application.production.RecipeCycleChecker;
|
||||
import de.effigenix.application.production.GetRecipe;
|
||||
|
|
@ -77,4 +80,20 @@ public class ProductionUseCaseConfiguration {
|
|||
BatchNumberGenerator batchNumberGenerator, AuthorizationPort authorizationPort) {
|
||||
return new PlanBatch(batchRepository, recipeRepository, batchNumberGenerator, authorizationPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GetBatch getBatch(BatchRepository batchRepository, AuthorizationPort authorizationPort) {
|
||||
return new GetBatch(batchRepository, authorizationPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ListBatches listBatches(BatchRepository batchRepository, RecipeRepository recipeRepository,
|
||||
AuthorizationPort authorizationPort) {
|
||||
return new ListBatches(batchRepository, recipeRepository, authorizationPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FindBatchByNumber findBatchByNumber(BatchRepository batchRepository, AuthorizationPort authorizationPort) {
|
||||
return new FindBatchByNumber(batchRepository, authorizationPort);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,17 @@ package de.effigenix.infrastructure.production.persistence.repository;
|
|||
import de.effigenix.infrastructure.production.persistence.entity.BatchEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BatchJpaRepository extends JpaRepository<BatchEntity, String> {
|
||||
|
||||
Optional<BatchEntity> findByBatchNumber(String batchNumber);
|
||||
|
||||
List<BatchEntity> findByStatus(String status);
|
||||
|
||||
List<BatchEntity> findByProductionDate(LocalDate productionDate);
|
||||
|
||||
List<BatchEntity> findByRecipeIdIn(List<String> recipeIds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import org.springframework.context.annotation.Profile;
|
|||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -54,6 +55,58 @@ public class JpaBatchRepository implements BatchRepository {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Optional<Batch>> findByBatchNumber(BatchNumber batchNumber) {
|
||||
try {
|
||||
Optional<Batch> result = jpaRepository.findByBatchNumber(batchNumber.value())
|
||||
.map(mapper::toDomain);
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
logger.trace("Database error in findByBatchNumber", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Batch>> findByStatus(BatchStatus status) {
|
||||
try {
|
||||
List<Batch> result = jpaRepository.findByStatus(status.name()).stream()
|
||||
.map(mapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
logger.trace("Database error in findByStatus", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Batch>> findByProductionDate(LocalDate date) {
|
||||
try {
|
||||
List<Batch> result = jpaRepository.findByProductionDate(date).stream()
|
||||
.map(mapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
logger.trace("Database error in findByProductionDate", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Batch>> findByRecipeIds(List<RecipeId> recipeIds) {
|
||||
try {
|
||||
List<String> ids = recipeIds.stream().map(RecipeId::value).toList();
|
||||
List<Batch> result = jpaRepository.findByRecipeIdIn(ids).stream()
|
||||
.map(mapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
logger.trace("Database error in findByRecipeIds", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Result<RepositoryError, Void> save(Batch batch) {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,19 @@ public class JpaRecipeRepository implements RecipeRepository {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Recipe>> findByArticleId(String articleId) {
|
||||
try {
|
||||
List<Recipe> result = jpaRepository.findByArticleId(articleId).stream()
|
||||
.map(mapper::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
logger.trace("Database error in findByArticleId", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Boolean> existsByNameAndVersion(String name, int version) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -10,4 +10,6 @@ public interface RecipeJpaRepository extends JpaRepository<RecipeEntity, String>
|
|||
List<RecipeEntity> findByStatus(String status);
|
||||
|
||||
boolean existsByNameAndVersion(String name, int version);
|
||||
|
||||
List<RecipeEntity> findByArticleId(String articleId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
package de.effigenix.infrastructure.production.web.controller;
|
||||
|
||||
import de.effigenix.application.production.FindBatchByNumber;
|
||||
import de.effigenix.application.production.GetBatch;
|
||||
import de.effigenix.application.production.ListBatches;
|
||||
import de.effigenix.application.production.PlanBatch;
|
||||
import de.effigenix.application.production.command.PlanBatchCommand;
|
||||
import de.effigenix.domain.production.BatchError;
|
||||
import de.effigenix.domain.production.BatchId;
|
||||
import de.effigenix.domain.production.BatchNumber;
|
||||
import de.effigenix.domain.production.BatchStatus;
|
||||
import de.effigenix.infrastructure.production.web.dto.BatchResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.BatchSummaryResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.PlanBatchRequest;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
|
|
@ -11,12 +18,16 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/production/batches")
|
||||
@SecurityRequirement(name = "Bearer Authentication")
|
||||
|
|
@ -26,9 +37,91 @@ public class BatchController {
|
|||
private static final Logger logger = LoggerFactory.getLogger(BatchController.class);
|
||||
|
||||
private final PlanBatch planBatch;
|
||||
private final GetBatch getBatch;
|
||||
private final ListBatches listBatches;
|
||||
private final FindBatchByNumber findBatchByNumber;
|
||||
|
||||
public BatchController(PlanBatch planBatch) {
|
||||
public BatchController(PlanBatch planBatch, GetBatch getBatch, ListBatches listBatches,
|
||||
FindBatchByNumber findBatchByNumber) {
|
||||
this.planBatch = planBatch;
|
||||
this.getBatch = getBatch;
|
||||
this.listBatches = listBatches;
|
||||
this.findBatchByNumber = findBatchByNumber;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('BATCH_READ')")
|
||||
public ResponseEntity<BatchResponse> getBatch(
|
||||
@PathVariable("id") String id,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = ActorId.of(authentication.getName());
|
||||
var result = getBatch.execute(BatchId.of(id), actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new BatchDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(BatchResponse.from(result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('BATCH_READ')")
|
||||
public ResponseEntity<List<BatchSummaryResponse>> listBatches(
|
||||
@RequestParam(value = "status", required = false) String status,
|
||||
@RequestParam(value = "productionDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate productionDate,
|
||||
@RequestParam(value = "articleId", required = false) String articleId,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = ActorId.of(authentication.getName());
|
||||
|
||||
var result = switch (filterType(status, productionDate, articleId)) {
|
||||
case "ambiguous" -> throw new BatchDomainErrorException(
|
||||
new BatchError.ValidationFailure("Only one filter allowed at a time: status, productionDate, or articleId"));
|
||||
case "status" -> {
|
||||
try {
|
||||
yield listBatches.executeByStatus(BatchStatus.valueOf(status), actorId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BatchDomainErrorException(
|
||||
new BatchError.ValidationFailure("Invalid status: " + status));
|
||||
}
|
||||
}
|
||||
case "productionDate" -> listBatches.executeByProductionDate(productionDate, actorId);
|
||||
case "articleId" -> listBatches.executeByArticleId(articleId, actorId);
|
||||
default -> listBatches.execute(actorId);
|
||||
};
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new BatchDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
var summaries = result.unsafeGetValue().stream()
|
||||
.map(BatchSummaryResponse::from)
|
||||
.toList();
|
||||
return ResponseEntity.ok(summaries);
|
||||
}
|
||||
|
||||
@GetMapping("/by-number/{batchNumber}")
|
||||
@PreAuthorize("hasAuthority('BATCH_READ')")
|
||||
public ResponseEntity<BatchResponse> findByNumber(
|
||||
@PathVariable("batchNumber") String batchNumber,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = ActorId.of(authentication.getName());
|
||||
|
||||
var parsedNumber = BatchNumber.of(batchNumber);
|
||||
if (parsedNumber.isFailure()) {
|
||||
throw new BatchDomainErrorException(
|
||||
new BatchError.ValidationFailure("Invalid batch number format: " + batchNumber));
|
||||
}
|
||||
|
||||
var result = findBatchByNumber.execute(parsedNumber.unsafeGetValue(), actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new BatchDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(BatchResponse.from(result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
|
@ -57,6 +150,15 @@ public class BatchController {
|
|||
.body(BatchResponse.from(result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
private static String filterType(String status, LocalDate productionDate, String articleId) {
|
||||
int count = (status != null ? 1 : 0) + (productionDate != null ? 1 : 0) + (articleId != null ? 1 : 0);
|
||||
if (count > 1) return "ambiguous";
|
||||
if (status != null) return "status";
|
||||
if (productionDate != null) return "productionDate";
|
||||
if (articleId != null) return "articleId";
|
||||
return "none";
|
||||
}
|
||||
|
||||
public static class BatchDomainErrorException extends RuntimeException {
|
||||
private final BatchError error;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package de.effigenix.infrastructure.production.web.dto;
|
||||
|
||||
import de.effigenix.domain.production.Batch;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public record BatchSummaryResponse(
|
||||
String id,
|
||||
String batchNumber,
|
||||
String recipeId,
|
||||
String status,
|
||||
String plannedQuantity,
|
||||
String plannedQuantityUnit,
|
||||
LocalDate productionDate,
|
||||
LocalDate bestBeforeDate,
|
||||
OffsetDateTime createdAt,
|
||||
OffsetDateTime updatedAt
|
||||
) {
|
||||
public static BatchSummaryResponse from(Batch batch) {
|
||||
return new BatchSummaryResponse(
|
||||
batch.id().value(),
|
||||
batch.batchNumber().value(),
|
||||
batch.recipeId().value(),
|
||||
batch.status().name(),
|
||||
batch.plannedQuantity().amount().toPlainString(),
|
||||
batch.plannedQuantity().uom().name(),
|
||||
batch.productionDate(),
|
||||
batch.bestBeforeDate(),
|
||||
batch.createdAt(),
|
||||
batch.updatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ public final class ProductionErrorHttpStatusMapper {
|
|||
public static int toHttpStatus(BatchError error) {
|
||||
return switch (error) {
|
||||
case BatchError.BatchNotFound e -> 404;
|
||||
case BatchError.BatchNotFoundByNumber e -> 404;
|
||||
case BatchError.InvalidPlannedQuantity e -> 400;
|
||||
case BatchError.InvalidDates e -> 400;
|
||||
case BatchError.RecipeNotActive e -> 409;
|
||||
|
|
|
|||
|
|
@ -48,4 +48,9 @@ public class StubRecipeRepository implements RecipeRepository {
|
|||
public Result<RepositoryError, List<Recipe>> findByStatus(RecipeStatus status) {
|
||||
return Result.failure(STUB_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Recipe>> findByArticleId(String articleId) {
|
||||
return Result.failure(STUB_ERROR);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
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.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("FindBatchByNumber Use Case")
|
||||
class FindBatchByNumberTest {
|
||||
|
||||
@Mock private BatchRepository batchRepository;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private FindBatchByNumber findBatchByNumber;
|
||||
private ActorId performedBy;
|
||||
|
||||
private static final BatchNumber BATCH_NUMBER = BatchNumber.generate(LocalDate.of(2026, 3, 1), 1);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
findBatchByNumber = new FindBatchByNumber(batchRepository, authPort);
|
||||
performedBy = ActorId.of("admin-user");
|
||||
}
|
||||
|
||||
private Batch sampleBatch() {
|
||||
return Batch.reconstitute(
|
||||
BatchId.of("batch-1"),
|
||||
BATCH_NUMBER,
|
||||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return batch when batch number exists")
|
||||
void should_ReturnBatch_When_BatchNumberExists() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByBatchNumber(BATCH_NUMBER)).thenReturn(Result.success(Optional.of(sampleBatch())));
|
||||
|
||||
var result = findBatchByNumber.execute(BATCH_NUMBER, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().batchNumber()).isEqualTo(BATCH_NUMBER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with BatchNotFoundByNumber when batch number does not exist")
|
||||
void should_FailWithBatchNotFoundByNumber_When_BatchNumberDoesNotExist() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByBatchNumber(BATCH_NUMBER)).thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = findBatchByNumber.execute(BATCH_NUMBER, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.BatchNotFoundByNumber.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when repository returns error")
|
||||
void should_FailWithRepositoryFailure_When_RepositoryReturnsError() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByBatchNumber(BATCH_NUMBER))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = findBatchByNumber.execute(BATCH_NUMBER, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||
void should_FailWithUnauthorized_When_ActorLacksPermission() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);
|
||||
|
||||
var result = findBatchByNumber.execute(BATCH_NUMBER, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
|
||||
verify(batchRepository, never()).findByBatchNumber(any());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
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.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("GetBatch Use Case")
|
||||
class GetBatchTest {
|
||||
|
||||
@Mock private BatchRepository batchRepository;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private GetBatch getBatch;
|
||||
private ActorId performedBy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
getBatch = new GetBatch(batchRepository, authPort);
|
||||
performedBy = ActorId.of("admin-user");
|
||||
}
|
||||
|
||||
private Batch sampleBatch(String id) {
|
||||
return Batch.reconstitute(
|
||||
BatchId.of(id),
|
||||
BatchNumber.generate(LocalDate.of(2026, 3, 1), 1),
|
||||
RecipeId.of("recipe-1"),
|
||||
BatchStatus.PLANNED,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
LocalDate.of(2026, 3, 1),
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return batch when batch exists")
|
||||
void should_ReturnBatch_When_BatchExists() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
var batch = sampleBatch("batch-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.of(batch)));
|
||||
|
||||
var result = getBatch.execute(batchId, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().id()).isEqualTo(batchId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with BatchNotFound when batch does not exist")
|
||||
void should_FailWithBatchNotFound_When_BatchDoesNotExist() {
|
||||
var batchId = BatchId.of("nonexistent");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId)).thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = getBatch.execute(batchId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.BatchNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when repository returns error")
|
||||
void should_FailWithRepositoryFailure_When_RepositoryReturnsError() {
|
||||
var batchId = BatchId.of("batch-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findById(batchId))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = getBatch.execute(batchId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||
void should_FailWithUnauthorized_When_ActorLacksPermission() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);
|
||||
|
||||
var result = getBatch.execute(BatchId.of("batch-1"), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
|
||||
verify(batchRepository, never()).findById(any());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
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.Nested;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("ListBatches Use Case")
|
||||
class ListBatchesTest {
|
||||
|
||||
@Mock private BatchRepository batchRepository;
|
||||
@Mock private RecipeRepository recipeRepository;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private ListBatches listBatches;
|
||||
private ActorId performedBy;
|
||||
|
||||
private static final LocalDate PRODUCTION_DATE = LocalDate.of(2026, 3, 1);
|
||||
private static final RepositoryError DB_ERROR = new RepositoryError.DatabaseError("connection lost");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
listBatches = new ListBatches(batchRepository, recipeRepository, authPort);
|
||||
performedBy = ActorId.of("admin-user");
|
||||
}
|
||||
|
||||
private Batch sampleBatch(String id, BatchStatus status) {
|
||||
return Batch.reconstitute(
|
||||
BatchId.of(id),
|
||||
BatchNumber.generate(PRODUCTION_DATE, 1),
|
||||
RecipeId.of("recipe-1"),
|
||||
status,
|
||||
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
|
||||
PRODUCTION_DATE,
|
||||
LocalDate.of(2026, 6, 1),
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
private Recipe sampleRecipe(String id) {
|
||||
return Recipe.reconstitute(
|
||||
RecipeId.of(id), new RecipeName("Bratwurst"), 1, RecipeType.FINISHED_PRODUCT,
|
||||
"Beschreibung", 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)
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("execute – alle Batches")
|
||||
class ExecuteAll {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return all batches")
|
||||
void should_ReturnAllBatches() {
|
||||
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED), sampleBatch("b2", BatchStatus.PLANNED));
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findAll()).thenReturn(Result.success(batches));
|
||||
|
||||
var result = listBatches.execute(performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no batches exist")
|
||||
void should_ReturnEmptyList_When_NoBatchesExist() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findAll()).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listBatches.execute(performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findAll fails")
|
||||
void should_FailWithRepositoryFailure_When_FindAllFails() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findAll()).thenReturn(Result.failure(DB_ERROR));
|
||||
|
||||
var result = listBatches.execute(performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||
void should_FailWithUnauthorized() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);
|
||||
|
||||
var result = listBatches.execute(performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
|
||||
verify(batchRepository, never()).findAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("executeByStatus – nach Status filtern")
|
||||
class ExecuteByStatus {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return batches filtered by status")
|
||||
void should_ReturnBatches_FilteredByStatus() {
|
||||
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByStatus(BatchStatus.PLANNED)).thenReturn(Result.success(batches));
|
||||
|
||||
var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no batches match status")
|
||||
void should_ReturnEmptyList_When_NoBatchesMatchStatus() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByStatus(BatchStatus.PLANNED)).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findByStatus fails")
|
||||
void should_FailWithRepositoryFailure_When_FindByStatusFails() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByStatus(BatchStatus.PLANNED)).thenReturn(Result.failure(DB_ERROR));
|
||||
|
||||
var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||
void should_FailWithUnauthorized() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);
|
||||
|
||||
var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
|
||||
verify(batchRepository, never()).findByStatus(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("executeByProductionDate – nach Datum filtern")
|
||||
class ExecuteByProductionDate {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return batches filtered by production date")
|
||||
void should_ReturnBatches_FilteredByProductionDate() {
|
||||
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByProductionDate(PRODUCTION_DATE)).thenReturn(Result.success(batches));
|
||||
|
||||
var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no batches match date")
|
||||
void should_ReturnEmptyList_When_NoBatchesMatchDate() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByProductionDate(PRODUCTION_DATE)).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findByProductionDate fails")
|
||||
void should_FailWithRepositoryFailure_When_FindByProductionDateFails() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(batchRepository.findByProductionDate(PRODUCTION_DATE)).thenReturn(Result.failure(DB_ERROR));
|
||||
|
||||
var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||
void should_FailWithUnauthorized() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);
|
||||
|
||||
var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
|
||||
verify(batchRepository, never()).findByProductionDate(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("executeByArticleId – über Recipe-Lookup filtern")
|
||||
class ExecuteByArticleId {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return batches filtered by articleId via recipe lookup")
|
||||
void should_ReturnBatches_FilteredByArticleId() {
|
||||
var recipe = sampleRecipe("recipe-1");
|
||||
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
|
||||
when(batchRepository.findByRecipeIds(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(batches));
|
||||
|
||||
var result = listBatches.executeByArticleId("article-123", performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no recipes match articleId")
|
||||
void should_ReturnEmptyList_When_NoRecipesMatchArticleId() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(recipeRepository.findByArticleId("unknown-article")).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listBatches.executeByArticleId("unknown-article", performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
verify(batchRepository, never()).findByRecipeIds(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when recipes found but no batches match")
|
||||
void should_ReturnEmptyList_When_RecipesFoundButNoBatches() {
|
||||
var recipe = sampleRecipe("recipe-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
|
||||
when(batchRepository.findByRecipeIds(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listBatches.executeByArticleId("article-123", performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when recipe repository fails")
|
||||
void should_FailWithRepositoryFailure_When_RecipeRepositoryFails() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.failure(DB_ERROR));
|
||||
|
||||
var result = listBatches.executeByArticleId("article-123", performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
verify(batchRepository, never()).findByRecipeIds(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when batch repository fails after recipe lookup")
|
||||
void should_FailWithRepositoryFailure_When_BatchRepositoryFailsAfterRecipeLookup() {
|
||||
var recipe = sampleRecipe("recipe-1");
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
|
||||
when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
|
||||
when(batchRepository.findByRecipeIds(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.failure(DB_ERROR));
|
||||
|
||||
var result = listBatches.executeByArticleId("article-123", performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with Unauthorized when actor lacks permission")
|
||||
void should_FailWithUnauthorized() {
|
||||
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);
|
||||
|
||||
var result = listBatches.executeByArticleId("article-123", performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);
|
||||
verify(recipeRepository, never()).findByArticleId(any());
|
||||
verify(batchRepository, never()).findByRecipeIds(any());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
package de.effigenix.infrastructure.production.web;
|
||||
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.infrastructure.AbstractIntegrationTest;
|
||||
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.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@DisplayName("GetBatch Integration Tests")
|
||||
class GetBatchIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RoleEntity adminRole = createRole(RoleName.ADMIN, "Admin");
|
||||
RoleEntity viewerRole = createRole(RoleName.PRODUCTION_WORKER, "Viewer");
|
||||
|
||||
UserEntity admin = createUser("batch.admin", "batch.admin@test.com", Set.of(adminRole), "BRANCH-01");
|
||||
UserEntity viewer = createUser("batch.viewer", "batch.viewer@test.com", Set.of(viewerRole), "BRANCH-01");
|
||||
|
||||
adminToken = generateToken(admin.getId(), "batch.admin", "BATCH_WRITE,BATCH_READ,RECIPE_WRITE,RECIPE_READ");
|
||||
viewerToken = generateToken(viewer.getId(), "batch.viewer", "USER_READ");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batch per ID abrufen → 200 mit vollständigen Daten")
|
||||
void getBatch_existingId_returns200WithFullDetails() throws Exception {
|
||||
String batchId = createBatch();
|
||||
|
||||
mockMvc.perform(get("/api/production/batches/{id}", batchId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(batchId))
|
||||
.andExpect(jsonPath("$.batchNumber").isNotEmpty())
|
||||
.andExpect(jsonPath("$.recipeId").isNotEmpty())
|
||||
.andExpect(jsonPath("$.status").value("PLANNED"))
|
||||
.andExpect(jsonPath("$.plannedQuantity").isNotEmpty())
|
||||
.andExpect(jsonPath("$.plannedQuantityUnit").value("KILOGRAM"))
|
||||
.andExpect(jsonPath("$.productionDate").value("2026-03-01"))
|
||||
.andExpect(jsonPath("$.bestBeforeDate").value("2026-06-01"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nicht existierender Batch → 404")
|
||||
void getBatch_nonExistentId_returns404() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches/{id}", "nonexistent-id")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batch abrufen ohne BATCH_READ → 403")
|
||||
void getBatch_withoutPermission_returns403() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches/{id}", "any-id")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batch abrufen ohne Token → 401")
|
||||
void getBatch_withoutToken_returns401() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches/{id}", "any-id"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batch per Number abrufen → 200")
|
||||
void findByNumber_existingNumber_returns200() throws Exception {
|
||||
createBatch();
|
||||
|
||||
mockMvc.perform(get("/api/production/batches/by-number/{batchNumber}", "P-2026-03-01-001")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.batchNumber").value("P-2026-03-01-001"))
|
||||
.andExpect(jsonPath("$.status").value("PLANNED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nicht existierende BatchNumber → 404")
|
||||
void findByNumber_nonExistent_returns404() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches/by-number/{batchNumber}", "P-2099-01-01-999")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_NOT_FOUND_BY_NUMBER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("BatchNumber-Suche ohne BATCH_READ → 403")
|
||||
void findByNumber_withoutPermission_returns403() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches/by-number/{batchNumber}", "P-2026-03-01-001")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("BatchNumber-Suche ohne Token → 401")
|
||||
void findByNumber_withoutToken_returns401() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches/by-number/{batchNumber}", "P-2026-03-01-001"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ungültiges BatchNumber-Format → 400")
|
||||
void findByNumber_invalidFormat_returns400() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches/by-number/{batchNumber}", "INVALID-FORMAT")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createBatch() throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
String json = """
|
||||
{
|
||||
"recipeId": "%s",
|
||||
"plannedQuantity": "100",
|
||||
"plannedQuantityUnit": "KILOGRAM",
|
||||
"productionDate": "2026-03-01",
|
||||
"bestBeforeDate": "2026-06-01"
|
||||
}
|
||||
""".formatted(recipeId);
|
||||
|
||||
var result = mockMvc.perform(post("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String createActiveRecipe() throws Exception {
|
||||
String recipeJson = """
|
||||
{
|
||||
"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(recipeJson))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
String recipeId = objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
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());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/activate", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
return recipeId;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
package de.effigenix.infrastructure.production.web;
|
||||
|
||||
import de.effigenix.domain.usermanagement.RoleName;
|
||||
import de.effigenix.infrastructure.AbstractIntegrationTest;
|
||||
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.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@DisplayName("ListBatches Integration Tests")
|
||||
class ListBatchesIntegrationTest extends AbstractIntegrationTest {
|
||||
|
||||
private String adminToken;
|
||||
private String viewerToken;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RoleEntity adminRole = createRole(RoleName.ADMIN, "Admin");
|
||||
RoleEntity viewerRole = createRole(RoleName.PRODUCTION_WORKER, "Viewer");
|
||||
|
||||
UserEntity admin = createUser("batch.admin", "batch.admin@test.com", Set.of(adminRole), "BRANCH-01");
|
||||
UserEntity viewer = createUser("batch.viewer", "batch.viewer@test.com", Set.of(viewerRole), "BRANCH-01");
|
||||
|
||||
adminToken = generateToken(admin.getId(), "batch.admin", "BATCH_WRITE,BATCH_READ,RECIPE_WRITE,RECIPE_READ");
|
||||
viewerToken = generateToken(viewer.getId(), "batch.viewer", "USER_READ");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Alle Batches auflisten → 200 mit Summary-Format")
|
||||
void listBatches_returnsAllWithSummaryFormat() throws Exception {
|
||||
createBatch("2026-03-01");
|
||||
createBatch("2026-03-02");
|
||||
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(2)))
|
||||
.andExpect(jsonPath("$[0].id").isNotEmpty())
|
||||
.andExpect(jsonPath("$[0].batchNumber").isNotEmpty())
|
||||
.andExpect(jsonPath("$[0].status").value("PLANNED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Leere Liste → 200 mit leerem Array")
|
||||
void listBatches_empty_returns200WithEmptyArray() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batches nach Status filtern → 200 nur passende")
|
||||
void listBatches_filteredByStatus_returnsOnlyMatching() throws Exception {
|
||||
createBatch("2026-03-01");
|
||||
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.param("status", "PLANNED")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(1)))
|
||||
.andExpect(jsonPath("$[0].status").value("PLANNED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ungültiger Status-Parameter → 400")
|
||||
void listBatches_invalidStatus_returns400() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.param("status", "INVALID")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batches nach Produktionsdatum filtern → 200")
|
||||
void listBatches_filteredByProductionDate_returnsOnlyMatching() throws Exception {
|
||||
createBatch("2026-03-01");
|
||||
createBatch("2026-03-02");
|
||||
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.param("productionDate", "2026-03-01")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(1)))
|
||||
.andExpect(jsonPath("$[0].productionDate").value("2026-03-01"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batches nach articleId filtern → 200")
|
||||
void listBatches_filteredByArticleId_returnsOnlyMatching() throws Exception {
|
||||
createBatch("2026-03-01");
|
||||
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.param("articleId", "article-123")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unbekannte articleId → 200 leeres Array")
|
||||
void listBatches_unknownArticleId_returnsEmptyArray() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.param("articleId", "unknown-article")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$", hasSize(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Mehrere Filter gleichzeitig → 400")
|
||||
void listBatches_multipleFilters_returns400() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.param("status", "PLANNED")
|
||||
.param("articleId", "article-123")
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_VALIDATION_ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batches auflisten ohne BATCH_READ → 403")
|
||||
void listBatches_withoutPermission_returns403() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Batches auflisten ohne Token → 401")
|
||||
void listBatches_withoutToken_returns401() throws Exception {
|
||||
mockMvc.perform(get("/api/production/batches"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private void createBatch(String productionDate) throws Exception {
|
||||
String recipeId = createActiveRecipe();
|
||||
|
||||
String json = """
|
||||
{
|
||||
"recipeId": "%s",
|
||||
"plannedQuantity": "100",
|
||||
"plannedQuantityUnit": "KILOGRAM",
|
||||
"productionDate": "%s",
|
||||
"bestBeforeDate": "2026-12-01"
|
||||
}
|
||||
""".formatted(recipeId, productionDate);
|
||||
|
||||
mockMvc.perform(post("/api/production/batches")
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(json))
|
||||
.andExpect(status().isCreated());
|
||||
}
|
||||
|
||||
private String createActiveRecipe() throws Exception {
|
||||
String recipeJson = """
|
||||
{
|
||||
"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(recipeJson))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
String recipeId = objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
|
||||
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());
|
||||
|
||||
mockMvc.perform(post("/api/recipes/{id}/activate", recipeId)
|
||||
.header("Authorization", "Bearer " + adminToken))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
return recipeId;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue