diff --git a/backend/src/main/java/de/effigenix/application/inventory/ActivateStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/ActivateStorageLocation.java new file mode 100644 index 0000000..7dc99ad --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/inventory/ActivateStorageLocation.java @@ -0,0 +1,47 @@ +package de.effigenix.application.inventory; + +import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public class ActivateStorageLocation { + + private final StorageLocationRepository storageLocationRepository; + + public ActivateStorageLocation(StorageLocationRepository storageLocationRepository) { + this.storageLocationRepository = storageLocationRepository; + } + + public Result execute(String storageLocationId, ActorId performedBy) { + // 1. Laden + var locationId = StorageLocationId.of(storageLocationId); + StorageLocation location; + switch (storageLocationRepository.findById(locationId)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var opt) -> { + if (opt.isEmpty()) { + return Result.failure(new StorageLocationError.StorageLocationNotFound(storageLocationId)); + } + location = opt.get(); + } + } + + // 2. Aktivieren + switch (location.activate()) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var ignored) -> { } + } + + // 3. Speichern + switch (storageLocationRepository.save(location)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + + return Result.success(location); + } +} diff --git a/backend/src/main/java/de/effigenix/application/inventory/DeactivateStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/DeactivateStorageLocation.java new file mode 100644 index 0000000..a11e1bb --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/inventory/DeactivateStorageLocation.java @@ -0,0 +1,50 @@ +package de.effigenix.application.inventory; + +import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public class DeactivateStorageLocation { + + private final StorageLocationRepository storageLocationRepository; + + public DeactivateStorageLocation(StorageLocationRepository storageLocationRepository) { + this.storageLocationRepository = storageLocationRepository; + } + + public Result execute(String storageLocationId, ActorId performedBy) { + // 1. Laden + var locationId = StorageLocationId.of(storageLocationId); + StorageLocation location; + switch (storageLocationRepository.findById(locationId)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var opt) -> { + if (opt.isEmpty()) { + return Result.failure(new StorageLocationError.StorageLocationNotFound(storageLocationId)); + } + location = opt.get(); + } + } + + // TODO: Stock-Existenz prüfen, wenn Stock BC implementiert ist + // Akzeptanzkriterium: Deaktivierung schlägt fehl, wenn Stock am Lagerort existiert + + // 2. Deaktivieren + switch (location.deactivate()) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var ignored) -> { } + } + + // 3. Speichern + switch (storageLocationRepository.save(location)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + + return Result.success(location); + } +} diff --git a/backend/src/main/java/de/effigenix/application/inventory/UpdateStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/UpdateStorageLocation.java new file mode 100644 index 0000000..3f49052 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/inventory/UpdateStorageLocation.java @@ -0,0 +1,62 @@ +package de.effigenix.application.inventory; + +import de.effigenix.application.inventory.command.UpdateStorageLocationCommand; +import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public class UpdateStorageLocation { + + private final StorageLocationRepository storageLocationRepository; + + public UpdateStorageLocation(StorageLocationRepository storageLocationRepository) { + this.storageLocationRepository = storageLocationRepository; + } + + public Result execute(UpdateStorageLocationCommand cmd, ActorId performedBy) { + // 1. Laden + var locationId = StorageLocationId.of(cmd.storageLocationId()); + StorageLocation location; + switch (storageLocationRepository.findById(locationId)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var opt) -> { + if (opt.isEmpty()) { + return Result.failure(new StorageLocationError.StorageLocationNotFound(cmd.storageLocationId())); + } + location = opt.get(); + } + } + + // 2. Draft bauen + Aggregate validieren lassen + var draft = new StorageLocationUpdateDraft(cmd.name(), cmd.minTemperature(), cmd.maxTemperature()); + switch (location.update(draft)) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var ignored) -> { } + } + + // 3. Uniqueness-Check (nur wenn Name geändert wurde) + if (cmd.name() != null) { + switch (storageLocationRepository.existsByNameAndIdNot(location.name(), locationId)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var exists) -> { + if (exists) { + return Result.failure(new StorageLocationError.NameAlreadyExists(cmd.name())); + } + } + } + } + + // 4. Speichern + switch (storageLocationRepository.save(location)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + + return Result.success(location); + } +} diff --git a/backend/src/main/java/de/effigenix/application/inventory/command/UpdateStorageLocationCommand.java b/backend/src/main/java/de/effigenix/application/inventory/command/UpdateStorageLocationCommand.java new file mode 100644 index 0000000..2ea390d --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/inventory/command/UpdateStorageLocationCommand.java @@ -0,0 +1,8 @@ +package de.effigenix.application.inventory.command; + +public record UpdateStorageLocationCommand( + String storageLocationId, + String name, + String minTemperature, + String maxTemperature +) {} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java b/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java index 8fb1e5b..4d6be55 100644 --- a/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java +++ b/backend/src/main/java/de/effigenix/domain/inventory/StorageLocationRepository.java @@ -18,5 +18,7 @@ public interface StorageLocationRepository { Result existsByName(StorageLocationName name); + Result existsByNameAndIdNot(StorageLocationName name, StorageLocationId id); + Result save(StorageLocation location); } diff --git a/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java b/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java index 5deebf5..2a80360 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java +++ b/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java @@ -1,6 +1,9 @@ package de.effigenix.infrastructure.config; +import de.effigenix.application.inventory.ActivateStorageLocation; import de.effigenix.application.inventory.CreateStorageLocation; +import de.effigenix.application.inventory.DeactivateStorageLocation; +import de.effigenix.application.inventory.UpdateStorageLocation; import de.effigenix.domain.inventory.StorageLocationRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,4 +17,19 @@ public class InventoryUseCaseConfiguration { public CreateStorageLocation createStorageLocation(StorageLocationRepository storageLocationRepository) { return new CreateStorageLocation(storageLocationRepository); } + + @Bean + public UpdateStorageLocation updateStorageLocation(StorageLocationRepository storageLocationRepository) { + return new UpdateStorageLocation(storageLocationRepository); + } + + @Bean + public DeactivateStorageLocation deactivateStorageLocation(StorageLocationRepository storageLocationRepository) { + return new DeactivateStorageLocation(storageLocationRepository); + } + + @Bean + public ActivateStorageLocation activateStorageLocation(StorageLocationRepository storageLocationRepository) { + return new ActivateStorageLocation(storageLocationRepository); + } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java index 3a2a4dd..0962300 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java @@ -90,6 +90,16 @@ public class JpaStorageLocationRepository implements StorageLocationRepository { } } + @Override + public Result existsByNameAndIdNot(StorageLocationName name, StorageLocationId id) { + try { + return Result.success(jpaRepository.existsByNameAndIdNot(name.value(), id.value())); + } catch (Exception e) { + logger.trace("Database error in existsByNameAndIdNot", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + @Override @Transactional public Result save(StorageLocation location) { diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java index b7af3c7..c04c09c 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java @@ -12,4 +12,6 @@ public interface StorageLocationJpaRepository extends JpaRepository findByActiveTrue(); boolean existsByName(String name); + + boolean existsByNameAndIdNot(String name, String id); } diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java index c439afd..92c4cdc 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StorageLocationController.java @@ -1,10 +1,15 @@ package de.effigenix.infrastructure.inventory.web.controller; +import de.effigenix.application.inventory.ActivateStorageLocation; import de.effigenix.application.inventory.CreateStorageLocation; +import de.effigenix.application.inventory.DeactivateStorageLocation; +import de.effigenix.application.inventory.UpdateStorageLocation; import de.effigenix.application.inventory.command.CreateStorageLocationCommand; +import de.effigenix.application.inventory.command.UpdateStorageLocationCommand; import de.effigenix.domain.inventory.StorageLocationError; import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest; import de.effigenix.infrastructure.inventory.web.dto.StorageLocationResponse; +import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest; import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -26,9 +31,20 @@ public class StorageLocationController { private static final Logger logger = LoggerFactory.getLogger(StorageLocationController.class); private final CreateStorageLocation createStorageLocation; + private final UpdateStorageLocation updateStorageLocation; + private final DeactivateStorageLocation deactivateStorageLocation; + private final ActivateStorageLocation activateStorageLocation; - public StorageLocationController(CreateStorageLocation createStorageLocation) { + public StorageLocationController( + CreateStorageLocation createStorageLocation, + UpdateStorageLocation updateStorageLocation, + DeactivateStorageLocation deactivateStorageLocation, + ActivateStorageLocation activateStorageLocation + ) { this.createStorageLocation = createStorageLocation; + this.updateStorageLocation = updateStorageLocation; + this.deactivateStorageLocation = deactivateStorageLocation; + this.activateStorageLocation = activateStorageLocation; } @PostMapping @@ -55,6 +71,68 @@ public class StorageLocationController { .body(StorageLocationResponse.from(result.unsafeGetValue())); } + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('STOCK_WRITE')") + public ResponseEntity updateStorageLocation( + @PathVariable("id") String storageLocationId, + @RequestBody UpdateStorageLocationRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Updating storage location: {} by actor: {}", storageLocationId, actorId.value()); + + var cmd = new UpdateStorageLocationCommand( + storageLocationId, request.name(), + request.minTemperature(), request.maxTemperature() + ); + var result = updateStorageLocation.execute(cmd, actorId); + + if (result.isFailure()) { + throw new StorageLocationDomainErrorException(result.unsafeGetError()); + } + + logger.info("Storage location updated: {}", storageLocationId); + return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue())); + } + + @PatchMapping("/{id}/deactivate") + @PreAuthorize("hasAuthority('STOCK_WRITE')") + public ResponseEntity deactivateStorageLocation( + @PathVariable("id") String storageLocationId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Deactivating storage location: {} by actor: {}", storageLocationId, actorId.value()); + + var result = deactivateStorageLocation.execute(storageLocationId, actorId); + + if (result.isFailure()) { + throw new StorageLocationDomainErrorException(result.unsafeGetError()); + } + + logger.info("Storage location deactivated: {}", storageLocationId); + return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue())); + } + + @PatchMapping("/{id}/activate") + @PreAuthorize("hasAuthority('STOCK_WRITE')") + public ResponseEntity activateStorageLocation( + @PathVariable("id") String storageLocationId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Activating storage location: {} by actor: {}", storageLocationId, actorId.value()); + + var result = activateStorageLocation.execute(storageLocationId, actorId); + + if (result.isFailure()) { + throw new StorageLocationDomainErrorException(result.unsafeGetError()); + } + + logger.info("Storage location activated: {}", storageLocationId); + return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue())); + } + private ActorId extractActorId(Authentication authentication) { if (authentication == null || authentication.getName() == null) { throw new IllegalStateException("No authentication found in SecurityContext"); diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/UpdateStorageLocationRequest.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/UpdateStorageLocationRequest.java new file mode 100644 index 0000000..ffb0e73 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/UpdateStorageLocationRequest.java @@ -0,0 +1,7 @@ +package de.effigenix.infrastructure.inventory.web.dto; + +public record UpdateStorageLocationRequest( + String name, + String minTemperature, + String maxTemperature +) {} diff --git a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java index 9ab7fac..1fac380 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StorageLocationControllerIntegrationTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.effigenix.domain.usermanagement.RoleName; import de.effigenix.domain.usermanagement.UserStatus; import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest; +import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest; import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity; import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity; import de.effigenix.infrastructure.usermanagement.persistence.repository.RoleJpaRepository; @@ -35,7 +36,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. /** * Integrationstests für StorageLocationController. * - * Abgedeckte Testfälle: Story 1.1 – Lagerort anlegen + * Abgedeckte Testfälle: + * - Story 1.1 – Lagerort anlegen + * - Story 1.2 – Lagerort bearbeiten und (de-)aktivieren */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @@ -273,8 +276,212 @@ class StorageLocationControllerIntegrationTest { } } + // ==================== Lagerort bearbeiten (Story 1.2) ==================== + + @Test + @DisplayName("Lagerort Name ändern → 200") + void updateStorageLocation_changeName_returns200() throws Exception { + String id = createAndReturnId("Update Test", "DRY_STORAGE", null, null); + + var request = new UpdateStorageLocationRequest("Neuer Name", null, null); + + mockMvc.perform(put("/api/inventory/storage-locations/" + id) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(id)) + .andExpect(jsonPath("$.name").value("Neuer Name")) + .andExpect(jsonPath("$.storageType").value("DRY_STORAGE")); + } + + @Test + @DisplayName("Lagerort Temperaturbereich ändern → 200") + void updateStorageLocation_changeTemperatureRange_returns200() throws Exception { + String id = createAndReturnId("Temp Update", "COLD_ROOM", "-2", "8"); + + var request = new UpdateStorageLocationRequest(null, "-5", "12"); + + mockMvc.perform(put("/api/inventory/storage-locations/" + id) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Temp Update")) + .andExpect(jsonPath("$.temperatureRange.minTemperature").value(-5)) + .andExpect(jsonPath("$.temperatureRange.maxTemperature").value(12)); + } + + @Test + @DisplayName("Lagerort mit ungültigem Namen aktualisieren → 400") + void updateStorageLocation_withBlankName_returns400() throws Exception { + String id = createAndReturnId("Blank Update", "DRY_STORAGE", null, null); + + var request = new UpdateStorageLocationRequest("", null, null); + + mockMvc.perform(put("/api/inventory/storage-locations/" + id) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_STORAGE_LOCATION_NAME")); + } + + @Test + @DisplayName("Lagerort mit doppeltem Namen aktualisieren → 409") + void updateStorageLocation_withDuplicateName_returns409() throws Exception { + createAndReturnId("Existing Name", "DRY_STORAGE", null, null); + String id = createAndReturnId("To Rename", "COLD_ROOM", null, null); + + var request = new UpdateStorageLocationRequest("Existing Name", null, null); + + mockMvc.perform(put("/api/inventory/storage-locations/" + id) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("STORAGE_LOCATION_NAME_EXISTS")); + } + + @Test + @DisplayName("Lagerort mit eigenem Namen aktualisieren → 200 (kein Duplikat)") + void updateStorageLocation_withOwnName_returns200() throws Exception { + String id = createAndReturnId("Same Name", "DRY_STORAGE", null, null); + + var request = new UpdateStorageLocationRequest("Same Name", null, null); + + mockMvc.perform(put("/api/inventory/storage-locations/" + id) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("Same Name")); + } + + @Test + @DisplayName("Nicht existierenden Lagerort aktualisieren → 404") + void updateStorageLocation_notFound_returns404() throws Exception { + var request = new UpdateStorageLocationRequest("New Name", null, null); + + mockMvc.perform(put("/api/inventory/storage-locations/" + UUID.randomUUID()) + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("STORAGE_LOCATION_NOT_FOUND")); + } + + @Test + @DisplayName("Lagerort aktualisieren ohne STOCK_WRITE → 403") + void updateStorageLocation_withViewerToken_returns403() throws Exception { + var request = new UpdateStorageLocationRequest("Name", null, null); + + mockMvc.perform(put("/api/inventory/storage-locations/" + UUID.randomUUID()) + .header("Authorization", "Bearer " + viewerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()); + } + + // ==================== Lagerort deaktivieren (Story 1.2) ==================== + + @Test + @DisplayName("Aktiven Lagerort deaktivieren → 200") + void deactivateStorageLocation_active_returns200() throws Exception { + String id = createAndReturnId("Deactivate Test", "DRY_STORAGE", null, null); + + mockMvc.perform(patch("/api/inventory/storage-locations/" + id + "/deactivate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(id)) + .andExpect(jsonPath("$.active").value(false)); + } + + @Test + @DisplayName("Bereits inaktiven Lagerort deaktivieren → 409") + void deactivateStorageLocation_alreadyInactive_returns409() throws Exception { + String id = createAndReturnId("Double Deactivate", "DRY_STORAGE", null, null); + + mockMvc.perform(patch("/api/inventory/storage-locations/" + id + "/deactivate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()); + + mockMvc.perform(patch("/api/inventory/storage-locations/" + id + "/deactivate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("ALREADY_INACTIVE")); + } + + @Test + @DisplayName("Nicht existierenden Lagerort deaktivieren → 404") + void deactivateStorageLocation_notFound_returns404() throws Exception { + mockMvc.perform(patch("/api/inventory/storage-locations/" + UUID.randomUUID() + "/deactivate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("STORAGE_LOCATION_NOT_FOUND")); + } + + @Test + @DisplayName("Lagerort deaktivieren ohne STOCK_WRITE → 403") + void deactivateStorageLocation_withViewerToken_returns403() throws Exception { + mockMvc.perform(patch("/api/inventory/storage-locations/" + UUID.randomUUID() + "/deactivate") + .header("Authorization", "Bearer " + viewerToken)) + .andExpect(status().isForbidden()); + } + + // ==================== Lagerort aktivieren (Story 1.2) ==================== + + @Test + @DisplayName("Inaktiven Lagerort aktivieren → 200") + void activateStorageLocation_inactive_returns200() throws Exception { + String id = createAndReturnId("Activate Test", "DRY_STORAGE", null, null); + + mockMvc.perform(patch("/api/inventory/storage-locations/" + id + "/deactivate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()); + + mockMvc.perform(patch("/api/inventory/storage-locations/" + id + "/activate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(id)) + .andExpect(jsonPath("$.active").value(true)); + } + + @Test + @DisplayName("Bereits aktiven Lagerort aktivieren → 409") + void activateStorageLocation_alreadyActive_returns409() throws Exception { + String id = createAndReturnId("Double Activate", "DRY_STORAGE", null, null); + + mockMvc.perform(patch("/api/inventory/storage-locations/" + id + "/activate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("ALREADY_ACTIVE")); + } + + @Test + @DisplayName("Nicht existierenden Lagerort aktivieren → 404") + void activateStorageLocation_notFound_returns404() throws Exception { + mockMvc.perform(patch("/api/inventory/storage-locations/" + UUID.randomUUID() + "/activate") + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("STORAGE_LOCATION_NOT_FOUND")); + } + // ==================== Hilfsmethoden ==================== + private String createAndReturnId(String name, String storageType, String minTemp, String maxTemp) throws Exception { + var request = new CreateStorageLocationRequest(name, storageType, minTemp, maxTemp); + + var result = mockMvc.perform(post("/api/inventory/storage-locations") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andReturn(); + + return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText(); + } + private String generateToken(String userId, String username, String permissions) { long now = System.currentTimeMillis(); javax.crypto.SecretKey key = io.jsonwebtoken.security.Keys.hmacShaKeyFor(