mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:19:35 +01:00
feat(inventory): Lagerorte abfragen mit Filter nach Typ und Status (#3)
Query UseCase: ListStorageLocations mit optionalen Filtern storageType und active GET /api/inventory/storage-locations mit STOCK_READ oder STOCK_WRITE Tests: 10 neue Integrationstests (35 gesamt)
This commit is contained in:
parent
9b9b7311d1
commit
6010820944
4 changed files with 224 additions and 2 deletions
|
|
@ -0,0 +1,51 @@
|
||||||
|
package de.effigenix.application.inventory;
|
||||||
|
|
||||||
|
import de.effigenix.domain.inventory.*;
|
||||||
|
import de.effigenix.shared.common.RepositoryError;
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ListStorageLocations {
|
||||||
|
|
||||||
|
private final StorageLocationRepository storageLocationRepository;
|
||||||
|
|
||||||
|
public ListStorageLocations(StorageLocationRepository storageLocationRepository) {
|
||||||
|
this.storageLocationRepository = storageLocationRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<StorageLocationError, List<StorageLocation>> execute(String storageType, Boolean active) {
|
||||||
|
if (storageType != null) {
|
||||||
|
return findByStorageType(storageType, active);
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(active)) {
|
||||||
|
return mapResult(storageLocationRepository.findActive());
|
||||||
|
}
|
||||||
|
return mapResult(storageLocationRepository.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<StorageLocationError, List<StorageLocation>> findByStorageType(String storageType, Boolean active) {
|
||||||
|
StorageType type;
|
||||||
|
try {
|
||||||
|
type = StorageType.valueOf(storageType);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.failure(new StorageLocationError.InvalidStorageType(storageType));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = mapResult(storageLocationRepository.findByStorageType(type));
|
||||||
|
if (result.isSuccess() && Boolean.TRUE.equals(active)) {
|
||||||
|
return Result.success(result.unsafeGetValue().stream().filter(StorageLocation::active).toList());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<StorageLocationError, List<StorageLocation>> mapResult(
|
||||||
|
Result<RepositoryError, List<StorageLocation>> result) {
|
||||||
|
return switch (result) {
|
||||||
|
case Result.Failure(var err) -> Result.failure(new StorageLocationError.RepositoryFailure(err.message()));
|
||||||
|
case Result.Success(var locations) -> Result.success(locations);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package de.effigenix.infrastructure.config;
|
||||||
import de.effigenix.application.inventory.ActivateStorageLocation;
|
import de.effigenix.application.inventory.ActivateStorageLocation;
|
||||||
import de.effigenix.application.inventory.CreateStorageLocation;
|
import de.effigenix.application.inventory.CreateStorageLocation;
|
||||||
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
||||||
|
import de.effigenix.application.inventory.ListStorageLocations;
|
||||||
import de.effigenix.application.inventory.UpdateStorageLocation;
|
import de.effigenix.application.inventory.UpdateStorageLocation;
|
||||||
import de.effigenix.domain.inventory.StorageLocationRepository;
|
import de.effigenix.domain.inventory.StorageLocationRepository;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
@ -32,4 +33,9 @@ public class InventoryUseCaseConfiguration {
|
||||||
public ActivateStorageLocation activateStorageLocation(StorageLocationRepository storageLocationRepository) {
|
public ActivateStorageLocation activateStorageLocation(StorageLocationRepository storageLocationRepository) {
|
||||||
return new ActivateStorageLocation(storageLocationRepository);
|
return new ActivateStorageLocation(storageLocationRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ListStorageLocations listStorageLocations(StorageLocationRepository storageLocationRepository) {
|
||||||
|
return new ListStorageLocations(storageLocationRepository);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package de.effigenix.infrastructure.inventory.web.controller;
|
||||||
import de.effigenix.application.inventory.ActivateStorageLocation;
|
import de.effigenix.application.inventory.ActivateStorageLocation;
|
||||||
import de.effigenix.application.inventory.CreateStorageLocation;
|
import de.effigenix.application.inventory.CreateStorageLocation;
|
||||||
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
||||||
|
import de.effigenix.application.inventory.ListStorageLocations;
|
||||||
import de.effigenix.application.inventory.UpdateStorageLocation;
|
import de.effigenix.application.inventory.UpdateStorageLocation;
|
||||||
import de.effigenix.application.inventory.command.CreateStorageLocationCommand;
|
import de.effigenix.application.inventory.command.CreateStorageLocationCommand;
|
||||||
import de.effigenix.application.inventory.command.UpdateStorageLocationCommand;
|
import de.effigenix.application.inventory.command.UpdateStorageLocationCommand;
|
||||||
|
|
@ -22,6 +23,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/inventory/storage-locations")
|
@RequestMapping("/api/inventory/storage-locations")
|
||||||
@SecurityRequirement(name = "Bearer Authentication")
|
@SecurityRequirement(name = "Bearer Authentication")
|
||||||
|
|
@ -34,17 +37,38 @@ public class StorageLocationController {
|
||||||
private final UpdateStorageLocation updateStorageLocation;
|
private final UpdateStorageLocation updateStorageLocation;
|
||||||
private final DeactivateStorageLocation deactivateStorageLocation;
|
private final DeactivateStorageLocation deactivateStorageLocation;
|
||||||
private final ActivateStorageLocation activateStorageLocation;
|
private final ActivateStorageLocation activateStorageLocation;
|
||||||
|
private final ListStorageLocations listStorageLocations;
|
||||||
|
|
||||||
public StorageLocationController(
|
public StorageLocationController(
|
||||||
CreateStorageLocation createStorageLocation,
|
CreateStorageLocation createStorageLocation,
|
||||||
UpdateStorageLocation updateStorageLocation,
|
UpdateStorageLocation updateStorageLocation,
|
||||||
DeactivateStorageLocation deactivateStorageLocation,
|
DeactivateStorageLocation deactivateStorageLocation,
|
||||||
ActivateStorageLocation activateStorageLocation
|
ActivateStorageLocation activateStorageLocation,
|
||||||
|
ListStorageLocations listStorageLocations
|
||||||
) {
|
) {
|
||||||
this.createStorageLocation = createStorageLocation;
|
this.createStorageLocation = createStorageLocation;
|
||||||
this.updateStorageLocation = updateStorageLocation;
|
this.updateStorageLocation = updateStorageLocation;
|
||||||
this.deactivateStorageLocation = deactivateStorageLocation;
|
this.deactivateStorageLocation = deactivateStorageLocation;
|
||||||
this.activateStorageLocation = activateStorageLocation;
|
this.activateStorageLocation = activateStorageLocation;
|
||||||
|
this.listStorageLocations = listStorageLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAuthority('STOCK_READ') or hasAuthority('STOCK_WRITE')")
|
||||||
|
public ResponseEntity<List<StorageLocationResponse>> listStorageLocations(
|
||||||
|
@RequestParam(value = "storageType", required = false) String storageType,
|
||||||
|
@RequestParam(value = "active", required = false) Boolean active
|
||||||
|
) {
|
||||||
|
var result = listStorageLocations.execute(storageType, active);
|
||||||
|
|
||||||
|
if (result.isFailure()) {
|
||||||
|
throw new StorageLocationDomainErrorException(result.unsafeGetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = result.unsafeGetValue().stream()
|
||||||
|
.map(StorageLocationResponse::from)
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ class StorageLocationControllerIntegrationTest {
|
||||||
private long jwtExpiration;
|
private long jwtExpiration;
|
||||||
|
|
||||||
private String adminToken;
|
private String adminToken;
|
||||||
|
private String readerToken;
|
||||||
private String viewerToken;
|
private String viewerToken;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
|
@ -87,13 +88,20 @@ class StorageLocationControllerIntegrationTest {
|
||||||
passwordEncoder.encode("Pass123"), Set.of(adminRole),
|
passwordEncoder.encode("Pass123"), Set.of(adminRole),
|
||||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||||
|
|
||||||
|
String readerId = UUID.randomUUID().toString();
|
||||||
|
userRepository.save(new UserEntity(
|
||||||
|
readerId, "inv.reader", "inv.reader@test.com",
|
||||||
|
passwordEncoder.encode("Pass123"), Set.of(viewerRole),
|
||||||
|
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||||
|
|
||||||
String viewerId = UUID.randomUUID().toString();
|
String viewerId = UUID.randomUUID().toString();
|
||||||
userRepository.save(new UserEntity(
|
userRepository.save(new UserEntity(
|
||||||
viewerId, "inv.viewer", "inv.viewer@test.com",
|
viewerId, "inv.viewer", "inv.viewer@test.com",
|
||||||
passwordEncoder.encode("Pass123"), Set.of(viewerRole),
|
passwordEncoder.encode("Pass123"), Set.of(viewerRole),
|
||||||
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
"BRANCH-01", UserStatus.ACTIVE, LocalDateTime.now(), null));
|
||||||
|
|
||||||
adminToken = generateToken(adminId, "inv.admin", "STOCK_WRITE");
|
adminToken = generateToken(adminId, "inv.admin", "STOCK_WRITE,STOCK_READ");
|
||||||
|
readerToken = generateToken(readerId, "inv.reader", "STOCK_READ");
|
||||||
viewerToken = generateToken(viewerId, "inv.viewer", "USER_READ");
|
viewerToken = generateToken(viewerId, "inv.viewer", "USER_READ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,6 +475,139 @@ class StorageLocationControllerIntegrationTest {
|
||||||
.andExpect(jsonPath("$.code").value("STORAGE_LOCATION_NOT_FOUND"));
|
.andExpect(jsonPath("$.code").value("STORAGE_LOCATION_NOT_FOUND"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Lagerorte abfragen (Story 1.3) ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Alle Lagerorte abfragen → 200 mit Liste")
|
||||||
|
void listStorageLocations_returnsAll() throws Exception {
|
||||||
|
createAndReturnId("Lager A", "DRY_STORAGE", null, null);
|
||||||
|
createAndReturnId("Lager B", "COLD_ROOM", "-2", "8");
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", hasSize(2)))
|
||||||
|
.andExpect(jsonPath("$[*].name", containsInAnyOrder("Lager A", "Lager B")))
|
||||||
|
.andExpect(jsonPath("$[0].id").isNotEmpty())
|
||||||
|
.andExpect(jsonPath("$[0].storageType").isNotEmpty())
|
||||||
|
.andExpect(jsonPath("$[0].active").isBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte nach StorageType filtern → 200")
|
||||||
|
void listStorageLocations_filterByStorageType() throws Exception {
|
||||||
|
createAndReturnId("Kühlraum", "COLD_ROOM", "-2", "8");
|
||||||
|
createAndReturnId("Trockenlager", "DRY_STORAGE", null, null);
|
||||||
|
createAndReturnId("Gefrierschrank", "FREEZER", "-25", "-18");
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.param("storageType", "COLD_ROOM")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", hasSize(1)))
|
||||||
|
.andExpect(jsonPath("$[0].storageType").value("COLD_ROOM"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte nach active filtern → 200")
|
||||||
|
void listStorageLocations_filterByActive() throws Exception {
|
||||||
|
String activeId = createAndReturnId("Aktiv", "DRY_STORAGE", null, null);
|
||||||
|
String inactiveId = createAndReturnId("Inaktiv", "COLD_ROOM", null, null);
|
||||||
|
|
||||||
|
// Deaktivieren
|
||||||
|
mockMvc.perform(patch("/api/inventory/storage-locations/" + inactiveId + "/deactivate")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.param("active", "true")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", hasSize(1)))
|
||||||
|
.andExpect(jsonPath("$[0].name").value("Aktiv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte mit storageType und active filtern → 200")
|
||||||
|
void listStorageLocations_filterByStorageTypeAndActive() throws Exception {
|
||||||
|
createAndReturnId("Kühl Aktiv", "COLD_ROOM", null, null);
|
||||||
|
String inactiveId = createAndReturnId("Kühl Inaktiv", "COLD_ROOM", null, null);
|
||||||
|
createAndReturnId("Trocken Aktiv", "DRY_STORAGE", null, null);
|
||||||
|
|
||||||
|
mockMvc.perform(patch("/api/inventory/storage-locations/" + inactiveId + "/deactivate")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.param("storageType", "COLD_ROOM")
|
||||||
|
.param("active", "true")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", hasSize(1)))
|
||||||
|
.andExpect(jsonPath("$[0].name").value("Kühl Aktiv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte mit ungültigem StorageType filtern → 400")
|
||||||
|
void listStorageLocations_invalidStorageType_returns400() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.param("storageType", "INVALID")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isBadRequest())
|
||||||
|
.andExpect(jsonPath("$.code").value("INVALID_STORAGE_TYPE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte abfragen mit STOCK_READ → 200")
|
||||||
|
void listStorageLocations_withReaderToken_returns200() throws Exception {
|
||||||
|
createAndReturnId("Reader Test", "DRY_STORAGE", null, null);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.header("Authorization", "Bearer " + readerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", hasSize(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte abfragen ohne passende Berechtigung → 403")
|
||||||
|
void listStorageLocations_withViewerToken_returns403() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.header("Authorization", "Bearer " + viewerToken))
|
||||||
|
.andExpect(status().isForbidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte abfragen ohne Token → 401")
|
||||||
|
void listStorageLocations_withoutToken_returns401() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations"))
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Leere Liste wenn keine Lagerorte existieren → 200")
|
||||||
|
void listStorageLocations_empty_returns200() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", hasSize(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Lagerorte enthalten vollständige Daten (id, name, storageType, temperatureRange, active)")
|
||||||
|
void listStorageLocations_returnsCompleteData() throws Exception {
|
||||||
|
createAndReturnId("Vollständig", "COLD_ROOM", "-2", "8");
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/inventory/storage-locations")
|
||||||
|
.header("Authorization", "Bearer " + adminToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$[0].id").isNotEmpty())
|
||||||
|
.andExpect(jsonPath("$[0].name").value("Vollständig"))
|
||||||
|
.andExpect(jsonPath("$[0].storageType").value("COLD_ROOM"))
|
||||||
|
.andExpect(jsonPath("$[0].temperatureRange.minTemperature").value(-2))
|
||||||
|
.andExpect(jsonPath("$[0].temperatureRange.maxTemperature").value(8))
|
||||||
|
.andExpect(jsonPath("$[0].active").value(true));
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Hilfsmethoden ====================
|
// ==================== Hilfsmethoden ====================
|
||||||
|
|
||||||
private String createAndReturnId(String name, String storageType, String minTemp, String maxTemp) throws Exception {
|
private String createAndReturnId(String name, String storageType, String minTemp, String maxTemp) throws Exception {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue