1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:30:16 +01:00

feat(inventory): StorageLocation bearbeiten und (de-)aktivieren (#2)

Use Cases: UpdateStorageLocation, DeactivateStorageLocation, ActivateStorageLocation
Endpoints: PUT /{id}, PATCH /{id}/deactivate, PATCH /{id}/activate
Repository: existsByNameAndIdNot für Uniqueness-Check bei Update
Tests: 14 neue Integrationstests (25 gesamt)
This commit is contained in:
Sebastian Frick 2026-02-19 10:10:57 +01:00
parent 05878b1ce9
commit 24a6869faf
11 changed files with 493 additions and 2 deletions

View file

@ -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<StorageLocationError, StorageLocation> 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);
}
}

View file

@ -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<StorageLocationError, StorageLocation> 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);
}
}

View file

@ -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<StorageLocationError, StorageLocation> 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);
}
}

View file

@ -0,0 +1,8 @@
package de.effigenix.application.inventory.command;
public record UpdateStorageLocationCommand(
String storageLocationId,
String name,
String minTemperature,
String maxTemperature
) {}

View file

@ -18,5 +18,7 @@ public interface StorageLocationRepository {
Result<RepositoryError, Boolean> existsByName(StorageLocationName name);
Result<RepositoryError, Boolean> existsByNameAndIdNot(StorageLocationName name, StorageLocationId id);
Result<RepositoryError, Void> save(StorageLocation location);
}

View file

@ -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);
}
}

View file

@ -90,6 +90,16 @@ public class JpaStorageLocationRepository implements StorageLocationRepository {
}
}
@Override
public Result<RepositoryError, Boolean> 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<RepositoryError, Void> save(StorageLocation location) {

View file

@ -12,4 +12,6 @@ public interface StorageLocationJpaRepository extends JpaRepository<StorageLocat
List<StorageLocationEntity> findByActiveTrue();
boolean existsByName(String name);
boolean existsByNameAndIdNot(String name, String id);
}

View file

@ -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<StorageLocationResponse> 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<StorageLocationResponse> 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<StorageLocationResponse> 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");

View file

@ -0,0 +1,7 @@
package de.effigenix.infrastructure.inventory.web.dto;
public record UpdateStorageLocationRequest(
String name,
String minTemperature,
String maxTemperature
) {}