diff --git a/backend/src/main/java/de/effigenix/application/production/ArchiveRecipe.java b/backend/src/main/java/de/effigenix/application/production/ArchiveRecipe.java new file mode 100644 index 0000000..05bc975 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/production/ArchiveRecipe.java @@ -0,0 +1,53 @@ +package de.effigenix.application.production; + +import de.effigenix.application.production.command.ArchiveRecipeCommand; +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 +public class ArchiveRecipe { + + private final RecipeRepository recipeRepository; + private final AuthorizationPort authorizationPort; + + public ArchiveRecipe(RecipeRepository recipeRepository, AuthorizationPort authorizationPort) { + this.recipeRepository = recipeRepository; + this.authorizationPort = authorizationPort; + } + + public Result execute(ArchiveRecipeCommand cmd, ActorId performedBy) { + if (!authorizationPort.can(performedBy, ProductionAction.RECIPE_WRITE)) { + return Result.failure(new RecipeError.Unauthorized("Not authorized to modify recipes")); + } + + var recipeId = RecipeId.of(cmd.recipeId()); + + Recipe recipe; + switch (recipeRepository.findById(recipeId)) { + case Result.Failure(var err) -> + { return Result.failure(new RecipeError.RepositoryFailure(err.message())); } + case Result.Success(var opt) -> { + if (opt.isEmpty()) { + return Result.failure(new RecipeError.RecipeNotFound(recipeId)); + } + recipe = opt.get(); + } + } + + switch (recipe.archive()) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var ignored) -> { } + } + + switch (recipeRepository.save(recipe)) { + case Result.Failure(var err) -> + { return Result.failure(new RecipeError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + + return Result.success(recipe); + } +} diff --git a/backend/src/main/java/de/effigenix/application/production/command/ArchiveRecipeCommand.java b/backend/src/main/java/de/effigenix/application/production/command/ArchiveRecipeCommand.java new file mode 100644 index 0000000..24017af --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/production/command/ArchiveRecipeCommand.java @@ -0,0 +1,4 @@ +package de.effigenix.application.production.command; + +public record ArchiveRecipeCommand(String recipeId) { +} diff --git a/backend/src/main/java/de/effigenix/domain/production/Recipe.java b/backend/src/main/java/de/effigenix/domain/production/Recipe.java index c1fc2fb..c6c6c35 100644 --- a/backend/src/main/java/de/effigenix/domain/production/Recipe.java +++ b/backend/src/main/java/de/effigenix/domain/production/Recipe.java @@ -29,6 +29,7 @@ import static de.effigenix.shared.common.Result.*; * 10. Step numbers must be unique within a recipe * 11. Recipe can only be activated when it has at least one ingredient * 12. Recipe can only be activated from DRAFT status + * 13. Recipe can only be archived from ACTIVE status */ public class Recipe { @@ -232,6 +233,15 @@ public class Recipe { return Result.success(null); } + public Result archive() { + if (status != RecipeStatus.ACTIVE) { + return Result.failure(new RecipeError.InvalidStatusTransition(status, RecipeStatus.ARCHIVED)); + } + this.status = RecipeStatus.ARCHIVED; + touch(); + return Result.success(null); + } + // ==================== Getters ==================== public RecipeId id() { return id; } diff --git a/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java b/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java index 58774cf..d6d9e3b 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java +++ b/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java @@ -1,6 +1,7 @@ package de.effigenix.infrastructure.config; import de.effigenix.application.production.ActivateRecipe; +import de.effigenix.application.production.ArchiveRecipe; import de.effigenix.application.production.AddProductionStep; import de.effigenix.application.production.AddRecipeIngredient; import de.effigenix.application.production.CreateRecipe; @@ -43,4 +44,9 @@ public class ProductionUseCaseConfiguration { public ActivateRecipe activateRecipe(RecipeRepository recipeRepository, AuthorizationPort authorizationPort) { return new ActivateRecipe(recipeRepository, authorizationPort); } + + @Bean + public ArchiveRecipe archiveRecipe(RecipeRepository recipeRepository, AuthorizationPort authorizationPort) { + return new ArchiveRecipe(recipeRepository, authorizationPort); + } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java index 5a297ae..ec19f1c 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/RecipeController.java @@ -1,12 +1,14 @@ package de.effigenix.infrastructure.production.web.controller; import de.effigenix.application.production.ActivateRecipe; +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.RemoveProductionStep; import de.effigenix.application.production.RemoveRecipeIngredient; import de.effigenix.application.production.command.ActivateRecipeCommand; +import de.effigenix.application.production.command.ArchiveRecipeCommand; import de.effigenix.application.production.command.AddProductionStepCommand; import de.effigenix.application.production.command.AddRecipeIngredientCommand; import de.effigenix.application.production.command.CreateRecipeCommand; @@ -43,19 +45,22 @@ public class RecipeController { private final AddProductionStep addProductionStep; private final RemoveProductionStep removeProductionStep; private final ActivateRecipe activateRecipe; + private final ArchiveRecipe archiveRecipe; public RecipeController(CreateRecipe createRecipe, AddRecipeIngredient addRecipeIngredient, RemoveRecipeIngredient removeRecipeIngredient, AddProductionStep addProductionStep, RemoveProductionStep removeProductionStep, - ActivateRecipe activateRecipe) { + ActivateRecipe activateRecipe, + ArchiveRecipe archiveRecipe) { this.createRecipe = createRecipe; this.addRecipeIngredient = addRecipeIngredient; this.removeRecipeIngredient = removeRecipeIngredient; this.addProductionStep = addProductionStep; this.removeProductionStep = removeProductionStep; this.activateRecipe = activateRecipe; + this.archiveRecipe = archiveRecipe; } @PostMapping @@ -192,6 +197,26 @@ public class RecipeController { return ResponseEntity.ok(RecipeResponse.from(result.unsafeGetValue())); } + @PostMapping("/{id}/archive") + @PreAuthorize("hasAuthority('RECIPE_WRITE')") + public ResponseEntity archiveRecipe( + @PathVariable("id") String recipeId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Archiving recipe: {} by actor: {}", recipeId, actorId.value()); + + var cmd = new ArchiveRecipeCommand(recipeId); + var result = archiveRecipe.execute(cmd, actorId); + + if (result.isFailure()) { + throw new RecipeDomainErrorException(result.unsafeGetError()); + } + + logger.info("Recipe archived: {}", recipeId); + return ResponseEntity.ok(RecipeResponse.from(result.unsafeGetValue())); + } + private ActorId extractActorId(Authentication authentication) { if (authentication == null || authentication.getName() == null) { throw new IllegalStateException("No authentication found in SecurityContext");