1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:29:35 +01:00

refactor(inventory): unsafeGet durch switch Pattern-Matching ersetzen, UserLookupPort einführen

unsafeGet-Aufrufe in allen 4 Inventory-Controllern und ListStorageLocations
durch typsicheres switch Pattern-Matching auf Result<E,T> ersetzt.

Neuer SharedKernel-Port UserLookupPort ermöglicht cross-BC Auflösung
von User-IDs zu Usernames (z.B. für initiatedBy/completedBy in
InventoryCountResponse).
This commit is contained in:
Sebastian Frick 2026-03-19 10:19:23 +01:00
parent e4f4537581
commit ae95a0284f
8 changed files with 264 additions and 246 deletions

View file

@ -18,16 +18,8 @@ public class ListStorageLocations {
if (storageType != null) { if (storageType != null) {
return findByStorageType(storageType, active); return findByStorageType(storageType, active);
} }
if (active == null) { return mapResult(storageLocationRepository.findAll())
return mapResult(storageLocationRepository.findAll()); .map(locations -> filterByActive(locations, active));
}
var result = mapResult(storageLocationRepository.findAll());
if (result.isSuccess()) {
return Result.success(result.unsafeGetValue().stream()
.filter(loc -> loc.active() == active)
.toList());
}
return result;
} }
private Result<StorageLocationError, List<StorageLocation>> findByStorageType(String storageType, Boolean active) { private Result<StorageLocationError, List<StorageLocation>> findByStorageType(String storageType, Boolean active) {
@ -38,13 +30,17 @@ public class ListStorageLocations {
return Result.failure(new StorageLocationError.InvalidStorageType(storageType)); return Result.failure(new StorageLocationError.InvalidStorageType(storageType));
} }
var result = mapResult(storageLocationRepository.findByStorageType(type)); return mapResult(storageLocationRepository.findByStorageType(type))
if (result.isSuccess() && active != null) { .map(locations -> filterByActive(locations, active));
return Result.success(result.unsafeGetValue().stream()
.filter(loc -> loc.active() == active)
.toList());
} }
return result;
private List<StorageLocation> filterByActive(List<StorageLocation> locations, Boolean active) {
if (active == null) {
return locations;
}
return locations.stream()
.filter(loc -> loc.active() == active)
.toList();
} }
private Result<StorageLocationError, List<StorageLocation>> mapResult( private Result<StorageLocationError, List<StorageLocation>> mapResult(

View file

@ -13,7 +13,9 @@ import de.effigenix.domain.inventory.InventoryCountError;
import de.effigenix.infrastructure.inventory.web.dto.CreateInventoryCountRequest; import de.effigenix.infrastructure.inventory.web.dto.CreateInventoryCountRequest;
import de.effigenix.infrastructure.inventory.web.dto.InventoryCountResponse; import de.effigenix.infrastructure.inventory.web.dto.InventoryCountResponse;
import de.effigenix.infrastructure.inventory.web.dto.RecordCountItemRequest; import de.effigenix.infrastructure.inventory.web.dto.RecordCountItemRequest;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.UserLookupPort;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
@ -37,19 +39,22 @@ public class InventoryCountController {
private final StartInventoryCount startInventoryCount; private final StartInventoryCount startInventoryCount;
private final RecordCountItem recordCountItem; private final RecordCountItem recordCountItem;
private final CompleteInventoryCount completeInventoryCount; private final CompleteInventoryCount completeInventoryCount;
private final UserLookupPort userLookup;
public InventoryCountController(CreateInventoryCount createInventoryCount, public InventoryCountController(CreateInventoryCount createInventoryCount,
GetInventoryCount getInventoryCount, GetInventoryCount getInventoryCount,
ListInventoryCounts listInventoryCounts, ListInventoryCounts listInventoryCounts,
StartInventoryCount startInventoryCount, StartInventoryCount startInventoryCount,
RecordCountItem recordCountItem, RecordCountItem recordCountItem,
CompleteInventoryCount completeInventoryCount) { CompleteInventoryCount completeInventoryCount,
UserLookupPort userLookup) {
this.createInventoryCount = createInventoryCount; this.createInventoryCount = createInventoryCount;
this.getInventoryCount = getInventoryCount; this.getInventoryCount = getInventoryCount;
this.listInventoryCounts = listInventoryCounts; this.listInventoryCounts = listInventoryCounts;
this.startInventoryCount = startInventoryCount; this.startInventoryCount = startInventoryCount;
this.recordCountItem = recordCountItem; this.recordCountItem = recordCountItem;
this.completeInventoryCount = completeInventoryCount; this.completeInventoryCount = completeInventoryCount;
this.userLookup = userLookup;
} }
@PostMapping @PostMapping
@ -64,14 +69,11 @@ public class InventoryCountController {
authentication.getName() authentication.getName()
); );
var result = createInventoryCount.execute(cmd, ActorId.of(authentication.getName())); return switch (createInventoryCount.execute(cmd, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var count) -> ResponseEntity.status(HttpStatus.CREATED)
throw new InventoryCountDomainErrorException(result.unsafeGetError()); .body(InventoryCountResponse.from(count, userLookup));
} };
return ResponseEntity.status(HttpStatus.CREATED)
.body(InventoryCountResponse.from(result.unsafeGetValue()));
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@ -80,13 +82,10 @@ public class InventoryCountController {
@PathVariable String id, @PathVariable String id,
Authentication authentication Authentication authentication
) { ) {
var result = getInventoryCount.execute(id, ActorId.of(authentication.getName())); return switch (getInventoryCount.execute(id, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var count) -> ResponseEntity.ok(InventoryCountResponse.from(count, userLookup));
throw new InventoryCountDomainErrorException(result.unsafeGetError()); };
}
return ResponseEntity.ok(InventoryCountResponse.from(result.unsafeGetValue()));
} }
@GetMapping @GetMapping
@ -95,16 +94,15 @@ public class InventoryCountController {
@RequestParam(required = false) String storageLocationId, @RequestParam(required = false) String storageLocationId,
Authentication authentication Authentication authentication
) { ) {
var result = listInventoryCounts.execute(storageLocationId, ActorId.of(authentication.getName())); return switch (listInventoryCounts.execute(storageLocationId, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var counts) -> {
throw new InventoryCountDomainErrorException(result.unsafeGetError()); var responses = counts.stream()
} .map(c -> InventoryCountResponse.from(c, userLookup))
List<InventoryCountResponse> responses = result.unsafeGetValue().stream()
.map(InventoryCountResponse::from)
.toList(); .toList();
return ResponseEntity.ok(responses); yield ResponseEntity.ok(responses);
}
};
} }
@PatchMapping("/{id}/start") @PatchMapping("/{id}/start")
@ -113,13 +111,10 @@ public class InventoryCountController {
@PathVariable String id, @PathVariable String id,
Authentication authentication Authentication authentication
) { ) {
var result = startInventoryCount.execute(id, ActorId.of(authentication.getName())); return switch (startInventoryCount.execute(id, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var count) -> ResponseEntity.ok(InventoryCountResponse.from(count, userLookup));
throw new InventoryCountDomainErrorException(result.unsafeGetError()); };
}
return ResponseEntity.ok(InventoryCountResponse.from(result.unsafeGetValue()));
} }
@PatchMapping("/{id}/items/{itemId}") @PatchMapping("/{id}/items/{itemId}")
@ -131,13 +126,10 @@ public class InventoryCountController {
Authentication authentication Authentication authentication
) { ) {
var cmd = new RecordCountItemCommand(id, itemId, request.actualQuantityAmount(), request.actualQuantityUnit()); var cmd = new RecordCountItemCommand(id, itemId, request.actualQuantityAmount(), request.actualQuantityUnit());
var result = recordCountItem.execute(cmd, ActorId.of(authentication.getName())); return switch (recordCountItem.execute(cmd, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var count) -> ResponseEntity.ok(InventoryCountResponse.from(count, userLookup));
throw new InventoryCountDomainErrorException(result.unsafeGetError()); };
}
return ResponseEntity.ok(InventoryCountResponse.from(result.unsafeGetValue()));
} }
@PostMapping("/{id}/complete") @PostMapping("/{id}/complete")
@ -147,13 +139,10 @@ public class InventoryCountController {
Authentication authentication Authentication authentication
) { ) {
var cmd = new CompleteInventoryCountCommand(id, authentication.getName()); var cmd = new CompleteInventoryCountCommand(id, authentication.getName());
var result = completeInventoryCount.execute(cmd, ActorId.of(authentication.getName())); return switch (completeInventoryCount.execute(cmd, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var count) -> ResponseEntity.ok(InventoryCountResponse.from(count, userLookup));
throw new InventoryCountDomainErrorException(result.unsafeGetError()); };
}
return ResponseEntity.ok(InventoryCountResponse.from(result.unsafeGetValue()));
} }
// ==================== Exception Wrapper ==================== // ==================== Exception Wrapper ====================

View file

@ -22,6 +22,7 @@ import de.effigenix.application.inventory.command.ReserveStockCommand;
import de.effigenix.application.inventory.command.UnblockStockBatchCommand; import de.effigenix.application.inventory.command.UnblockStockBatchCommand;
import de.effigenix.application.inventory.command.UpdateStockCommand; import de.effigenix.application.inventory.command.UpdateStockCommand;
import de.effigenix.domain.inventory.StockError; import de.effigenix.domain.inventory.StockError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest; import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest;
import de.effigenix.infrastructure.inventory.web.dto.BlockStockBatchRequest; import de.effigenix.infrastructure.inventory.web.dto.BlockStockBatchRequest;
@ -93,44 +94,39 @@ public class StockController {
@RequestParam(required = false) String storageLocationId, @RequestParam(required = false) String storageLocationId,
@RequestParam(required = false) String articleId @RequestParam(required = false) String articleId
) { ) {
var result = listStocks.execute(storageLocationId, articleId); return switch (listStocks.execute(storageLocationId, articleId)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var stocks) -> {
throw new StockDomainErrorException(result.unsafeGetError()); var responses = stocks.stream()
}
List<StockResponse> responses = result.unsafeGetValue().stream()
.map(StockResponse::from) .map(StockResponse::from)
.toList(); .toList();
return ResponseEntity.ok(responses); yield ResponseEntity.ok(responses);
}
};
} }
// NOTE: Must be declared before /{id} to avoid Spring matching "below-minimum" as path variable // NOTE: Must be declared before /{id} to avoid Spring matching "below-minimum" as path variable
@GetMapping("/below-minimum") @GetMapping("/below-minimum")
@PreAuthorize("hasAuthority('STOCK_READ')") @PreAuthorize("hasAuthority('STOCK_READ')")
public ResponseEntity<List<StockResponse>> listStocksBelowMinimum(Authentication authentication) { public ResponseEntity<List<StockResponse>> listStocksBelowMinimum(Authentication authentication) {
var result = listStocksBelowMinimum.execute(ActorId.of(authentication.getName())); return switch (listStocksBelowMinimum.execute(ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var stocks) -> {
throw new StockDomainErrorException(result.unsafeGetError()); var responses = stocks.stream()
}
List<StockResponse> responses = result.unsafeGetValue().stream()
.map(StockResponse::from) .map(StockResponse::from)
.toList(); .toList();
return ResponseEntity.ok(responses); yield ResponseEntity.ok(responses);
}
};
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasAuthority('STOCK_READ')") @PreAuthorize("hasAuthority('STOCK_READ')")
public ResponseEntity<StockResponse> getStock(@PathVariable String id) { public ResponseEntity<StockResponse> getStock(@PathVariable String id) {
var result = getStock.execute(id); return switch (getStock.execute(id)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var stock) -> ResponseEntity.ok(StockResponse.from(stock));
throw new StockDomainErrorException(result.unsafeGetError()); };
}
return ResponseEntity.ok(StockResponse.from(result.unsafeGetValue()));
} }
@PostMapping @PostMapping
@ -147,15 +143,14 @@ public class StockController {
request.minimumLevelAmount(), request.minimumLevelUnit(), request.minimumLevelAmount(), request.minimumLevelUnit(),
request.minimumShelfLifeDays() request.minimumShelfLifeDays()
); );
var result = createStock.execute(cmd); return switch (createStock.execute(cmd)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var stock) -> {
throw new StockDomainErrorException(result.unsafeGetError()); logger.info("Stock created: {}", stock.id().value());
yield ResponseEntity.status(HttpStatus.CREATED)
.body(CreateStockResponse.from(stock));
} }
};
logger.info("Stock created: {}", result.unsafeGetValue().id().value());
return ResponseEntity.status(HttpStatus.CREATED)
.body(CreateStockResponse.from(result.unsafeGetValue()));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@ -171,14 +166,13 @@ public class StockController {
id, request.minimumLevelAmount(), request.minimumLevelUnit(), id, request.minimumLevelAmount(), request.minimumLevelUnit(),
request.minimumShelfLifeDays() request.minimumShelfLifeDays()
); );
var result = updateStock.execute(cmd); return switch (updateStock.execute(cmd)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var stock) -> {
throw new StockDomainErrorException(result.unsafeGetError());
}
logger.info("Stock updated: {}", id); logger.info("Stock updated: {}", id);
return ResponseEntity.ok(StockResponse.from(result.unsafeGetValue())); yield ResponseEntity.ok(StockResponse.from(stock));
}
};
} }
@PostMapping("/{stockId}/batches") @PostMapping("/{stockId}/batches")
@ -195,15 +189,14 @@ public class StockController {
request.quantityAmount(), request.quantityUnit(), request.quantityAmount(), request.quantityUnit(),
request.expiryDate() request.expiryDate()
); );
var result = addStockBatch.execute(cmd); return switch (addStockBatch.execute(cmd)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var batch) -> {
throw new StockDomainErrorException(result.unsafeGetError()); logger.info("Batch added: {}", batch.id().value());
yield ResponseEntity.status(HttpStatus.CREATED)
.body(StockBatchResponse.from(batch));
} }
};
logger.info("Batch added: {}", result.unsafeGetValue().id().value());
return ResponseEntity.status(HttpStatus.CREATED)
.body(StockBatchResponse.from(result.unsafeGetValue()));
} }
@PostMapping("/{stockId}/batches/{batchId}/remove") @PostMapping("/{stockId}/batches/{batchId}/remove")
@ -220,14 +213,13 @@ public class StockController {
stockId, batchId, stockId, batchId,
request.quantityAmount(), request.quantityUnit() request.quantityAmount(), request.quantityUnit()
); );
var result = removeStockBatch.execute(cmd); return switch (removeStockBatch.execute(cmd)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var ignored) -> {
throw new StockDomainErrorException(result.unsafeGetError());
}
logger.info("Batch removal completed for batch {} of stock {}", batchId, stockId); logger.info("Batch removal completed for batch {} of stock {}", batchId, stockId);
return ResponseEntity.ok().build(); yield ResponseEntity.ok().build();
}
};
} }
@PostMapping("/{stockId}/batches/{batchId}/block") @PostMapping("/{stockId}/batches/{batchId}/block")
@ -241,14 +233,13 @@ public class StockController {
logger.info("Blocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName()); logger.info("Blocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName());
var cmd = new BlockStockBatchCommand(stockId, batchId, request.reason()); var cmd = new BlockStockBatchCommand(stockId, batchId, request.reason());
var result = blockStockBatch.execute(cmd, ActorId.of(authentication.getName())); return switch (blockStockBatch.execute(cmd, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var ignored) -> {
throw new StockDomainErrorException(result.unsafeGetError());
}
logger.info("Batch {} of stock {} blocked", batchId, stockId); logger.info("Batch {} of stock {} blocked", batchId, stockId);
return ResponseEntity.ok().build(); yield ResponseEntity.ok().build();
}
};
} }
@PostMapping("/{stockId}/batches/{batchId}/unblock") @PostMapping("/{stockId}/batches/{batchId}/unblock")
@ -261,14 +252,13 @@ public class StockController {
logger.info("Unblocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName()); logger.info("Unblocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName());
var cmd = new UnblockStockBatchCommand(stockId, batchId); var cmd = new UnblockStockBatchCommand(stockId, batchId);
var result = unblockStockBatch.execute(cmd, ActorId.of(authentication.getName())); return switch (unblockStockBatch.execute(cmd, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var ignored) -> {
throw new StockDomainErrorException(result.unsafeGetError());
}
logger.info("Batch {} of stock {} unblocked", batchId, stockId); logger.info("Batch {} of stock {} unblocked", batchId, stockId);
return ResponseEntity.ok().build(); yield ResponseEntity.ok().build();
}
};
} }
@PostMapping("/{stockId}/reservations") @PostMapping("/{stockId}/reservations")
@ -284,15 +274,14 @@ public class StockController {
stockId, request.referenceType(), request.referenceId(), stockId, request.referenceType(), request.referenceId(),
request.quantityAmount(), request.quantityUnit(), request.priority() request.quantityAmount(), request.quantityUnit(), request.priority()
); );
var result = reserveStock.execute(cmd); return switch (reserveStock.execute(cmd)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var reservation) -> {
throw new StockDomainErrorException(result.unsafeGetError()); logger.info("Reservation created: {} for stock {}", reservation.id().value(), stockId);
yield ResponseEntity.status(HttpStatus.CREATED)
.body(ReservationResponse.from(reservation));
} }
};
logger.info("Reservation created: {} for stock {}", result.unsafeGetValue().id().value(), stockId);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ReservationResponse.from(result.unsafeGetValue()));
} }
@DeleteMapping("/{stockId}/reservations/{reservationId}") @DeleteMapping("/{stockId}/reservations/{reservationId}")
@ -305,14 +294,13 @@ public class StockController {
logger.info("Releasing reservation {} of stock {} by actor: {}", reservationId, stockId, authentication.getName()); logger.info("Releasing reservation {} of stock {} by actor: {}", reservationId, stockId, authentication.getName());
var cmd = new ReleaseReservationCommand(stockId, reservationId); var cmd = new ReleaseReservationCommand(stockId, reservationId);
var result = releaseReservation.execute(cmd); return switch (releaseReservation.execute(cmd)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var ignored) -> {
throw new StockDomainErrorException(result.unsafeGetError());
}
logger.info("Reservation {} of stock {} released", reservationId, stockId); logger.info("Reservation {} of stock {} released", reservationId, stockId);
return ResponseEntity.noContent().build(); yield ResponseEntity.noContent().build();
}
};
} }
@PostMapping("/{stockId}/reservations/{reservationId}/confirm") @PostMapping("/{stockId}/reservations/{reservationId}/confirm")
@ -325,14 +313,13 @@ public class StockController {
logger.info("Confirming reservation {} of stock {} by actor: {}", reservationId, stockId, authentication.getName()); logger.info("Confirming reservation {} of stock {} by actor: {}", reservationId, stockId, authentication.getName());
var cmd = new ConfirmReservationCommand(stockId, reservationId, authentication.getName()); var cmd = new ConfirmReservationCommand(stockId, reservationId, authentication.getName());
var result = confirmReservation.execute(cmd); return switch (confirmReservation.execute(cmd)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var ignored) -> {
throw new StockDomainErrorException(result.unsafeGetError());
}
logger.info("Reservation {} of stock {} confirmed", reservationId, stockId); logger.info("Reservation {} of stock {} confirmed", reservationId, stockId);
return ResponseEntity.noContent().build(); yield ResponseEntity.noContent().build();
}
};
} }
public static class StockDomainErrorException extends RuntimeException { public static class StockDomainErrorException extends RuntimeException {

View file

@ -7,6 +7,7 @@ import de.effigenix.application.inventory.command.RecordStockMovementCommand;
import de.effigenix.domain.inventory.StockMovementError; import de.effigenix.domain.inventory.StockMovementError;
import de.effigenix.infrastructure.inventory.web.dto.RecordStockMovementRequest; import de.effigenix.infrastructure.inventory.web.dto.RecordStockMovementRequest;
import de.effigenix.infrastructure.inventory.web.dto.StockMovementResponse; import de.effigenix.infrastructure.inventory.web.dto.StockMovementResponse;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@ -61,15 +62,14 @@ public class StockMovementController {
request.reason(), request.referenceDocumentId(), request.reason(), request.referenceDocumentId(),
authentication.getName() authentication.getName()
); );
var result = recordStockMovement.execute(cmd, ActorId.of(authentication.getName())); return switch (recordStockMovement.execute(cmd, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new StockMovementDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var movement) -> {
throw new StockMovementDomainErrorException(result.unsafeGetError()); logger.info("Stock movement recorded: {}", movement.id().value());
yield ResponseEntity.status(HttpStatus.CREATED)
.body(StockMovementResponse.from(movement));
} }
};
logger.info("Stock movement recorded: {}", result.unsafeGetValue().id().value());
return ResponseEntity.status(HttpStatus.CREATED)
.body(StockMovementResponse.from(result.unsafeGetValue()));
} }
@GetMapping @GetMapping
@ -85,31 +85,26 @@ public class StockMovementController {
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to,
Authentication authentication Authentication authentication
) { ) {
var result = listStockMovements.execute(stockId, articleId, movementType, return switch (listStockMovements.execute(stockId, articleId, movementType,
batchReference, from, to, batchReference, from, to, ActorId.of(authentication.getName()))) {
ActorId.of(authentication.getName())); case Result.Failure(var err) -> throw new StockMovementDomainErrorException(err);
case Result.Success(var movements) -> {
if (result.isFailure()) { var responses = movements.stream()
throw new StockMovementDomainErrorException(result.unsafeGetError());
}
List<StockMovementResponse> responses = result.unsafeGetValue().stream()
.map(StockMovementResponse::from) .map(StockMovementResponse::from)
.toList(); .toList();
return ResponseEntity.ok(responses); yield ResponseEntity.ok(responses);
}
};
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasAuthority('STOCK_MOVEMENT_READ')") @PreAuthorize("hasAuthority('STOCK_MOVEMENT_READ')")
public ResponseEntity<StockMovementResponse> getMovement(@PathVariable String id, public ResponseEntity<StockMovementResponse> getMovement(@PathVariable String id,
Authentication authentication) { Authentication authentication) {
var result = getStockMovement.execute(id, ActorId.of(authentication.getName())); return switch (getStockMovement.execute(id, ActorId.of(authentication.getName()))) {
case Result.Failure(var err) -> throw new StockMovementDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var movement) -> ResponseEntity.ok(StockMovementResponse.from(movement));
throw new StockMovementDomainErrorException(result.unsafeGetError()); };
}
return ResponseEntity.ok(StockMovementResponse.from(result.unsafeGetValue()));
} }
public static class StockMovementDomainErrorException extends RuntimeException { public static class StockMovementDomainErrorException extends RuntimeException {

View file

@ -12,6 +12,7 @@ import de.effigenix.domain.inventory.StorageLocationError;
import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest; import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest;
import de.effigenix.infrastructure.inventory.web.dto.StorageLocationResponse; import de.effigenix.infrastructure.inventory.web.dto.StorageLocationResponse;
import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest; import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -63,16 +64,15 @@ public class StorageLocationController {
@RequestParam(value = "storageType", required = false) String storageType, @RequestParam(value = "storageType", required = false) String storageType,
@RequestParam(value = "active", required = false) Boolean active @RequestParam(value = "active", required = false) Boolean active
) { ) {
var result = listStorageLocations.execute(storageType, active); return switch (listStorageLocations.execute(storageType, active)) {
case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var locations) -> {
throw new StorageLocationDomainErrorException(result.unsafeGetError()); var response = locations.stream()
}
var response = result.unsafeGetValue().stream()
.map(StorageLocationResponse::from) .map(StorageLocationResponse::from)
.toList(); .toList();
return ResponseEntity.ok(response); yield ResponseEntity.ok(response);
}
};
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@ -82,13 +82,10 @@ public class StorageLocationController {
Authentication authentication Authentication authentication
) { ) {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
var result = getStorageLocation.execute(storageLocationId, actorId); return switch (getStorageLocation.execute(storageLocationId, actorId)) {
case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var location) -> ResponseEntity.ok(StorageLocationResponse.from(location));
throw new StorageLocationDomainErrorException(result.unsafeGetError()); };
}
return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue()));
} }
@PostMapping @PostMapping
@ -104,15 +101,14 @@ public class StorageLocationController {
request.name(), request.storageType(), request.name(), request.storageType(),
request.minTemperature(), request.maxTemperature() request.minTemperature(), request.maxTemperature()
); );
var result = createStorageLocation.execute(cmd, actorId); return switch (createStorageLocation.execute(cmd, actorId)) {
case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var location) -> {
throw new StorageLocationDomainErrorException(result.unsafeGetError());
}
logger.info("Storage location created: {}", request.name()); logger.info("Storage location created: {}", request.name());
return ResponseEntity.status(HttpStatus.CREATED) yield ResponseEntity.status(HttpStatus.CREATED)
.body(StorageLocationResponse.from(result.unsafeGetValue())); .body(StorageLocationResponse.from(location));
}
};
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@ -129,14 +125,13 @@ public class StorageLocationController {
storageLocationId, request.name(), storageLocationId, request.name(),
request.minTemperature(), request.maxTemperature() request.minTemperature(), request.maxTemperature()
); );
var result = updateStorageLocation.execute(cmd, actorId); return switch (updateStorageLocation.execute(cmd, actorId)) {
case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var location) -> {
throw new StorageLocationDomainErrorException(result.unsafeGetError());
}
logger.info("Storage location updated: {}", storageLocationId); logger.info("Storage location updated: {}", storageLocationId);
return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue())); yield ResponseEntity.ok(StorageLocationResponse.from(location));
}
};
} }
@PatchMapping("/{id}/deactivate") @PatchMapping("/{id}/deactivate")
@ -148,14 +143,13 @@ public class StorageLocationController {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
logger.info("Deactivating storage location: {} by actor: {}", storageLocationId, actorId.value()); logger.info("Deactivating storage location: {} by actor: {}", storageLocationId, actorId.value());
var result = deactivateStorageLocation.execute(storageLocationId, actorId); return switch (deactivateStorageLocation.execute(storageLocationId, actorId)) {
case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var location) -> {
throw new StorageLocationDomainErrorException(result.unsafeGetError());
}
logger.info("Storage location deactivated: {}", storageLocationId); logger.info("Storage location deactivated: {}", storageLocationId);
return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue())); yield ResponseEntity.ok(StorageLocationResponse.from(location));
}
};
} }
@PatchMapping("/{id}/activate") @PatchMapping("/{id}/activate")
@ -167,14 +161,13 @@ public class StorageLocationController {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
logger.info("Activating storage location: {} by actor: {}", storageLocationId, actorId.value()); logger.info("Activating storage location: {} by actor: {}", storageLocationId, actorId.value());
var result = activateStorageLocation.execute(storageLocationId, actorId); return switch (activateStorageLocation.execute(storageLocationId, actorId)) {
case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err);
if (result.isFailure()) { case Result.Success(var location) -> {
throw new StorageLocationDomainErrorException(result.unsafeGetError());
}
logger.info("Storage location activated: {}", storageLocationId); logger.info("Storage location activated: {}", storageLocationId);
return ResponseEntity.ok(StorageLocationResponse.from(result.unsafeGetValue())); yield ResponseEntity.ok(StorageLocationResponse.from(location));
}
};
} }
private ActorId extractActorId(Authentication authentication) { private ActorId extractActorId(Authentication authentication) {

View file

@ -1,6 +1,7 @@
package de.effigenix.infrastructure.inventory.web.dto; package de.effigenix.infrastructure.inventory.web.dto;
import de.effigenix.domain.inventory.InventoryCount; import de.effigenix.domain.inventory.InventoryCount;
import de.effigenix.shared.security.UserLookupPort;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
@ -16,13 +17,15 @@ public record InventoryCountResponse(
Instant createdAt, Instant createdAt,
List<CountItemResponse> countItems List<CountItemResponse> countItems
) { ) {
public static InventoryCountResponse from(InventoryCount count) { public static InventoryCountResponse from(InventoryCount count, UserLookupPort userLookup) {
return new InventoryCountResponse( return new InventoryCountResponse(
count.id().value(), count.id().value(),
count.storageLocationId().value(), count.storageLocationId().value(),
count.countDate(), count.countDate(),
count.initiatedBy(), userLookup.resolveUsername(count.initiatedBy()).orElse(count.initiatedBy()),
count.completedBy(), count.completedBy() != null
? userLookup.resolveUsername(count.completedBy()).orElse(count.completedBy())
: null,
count.status().name(), count.status().name(),
count.createdAt(), count.createdAt(),
count.countItems().stream() count.countItems().stream()

View file

@ -0,0 +1,33 @@
package de.effigenix.infrastructure.security;
import de.effigenix.domain.usermanagement.User;
import de.effigenix.domain.usermanagement.UserId;
import de.effigenix.domain.usermanagement.UserRepository;
import de.effigenix.shared.security.UserLookupPort;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* Spring implementation of UserLookupPort.
*
* Resolves user IDs to usernames via UserRepository.
*/
@Component
public class SpringUserLookupAdapter implements UserLookupPort {
private final UserRepository userRepository;
public SpringUserLookupAdapter(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Optional<String> resolveUsername(String userId) {
return userRepository.findById(UserId.of(userId))
.fold(
err -> Optional.empty(),
opt -> opt.map(User::username)
);
}
}

View file

@ -0,0 +1,22 @@
package de.effigenix.shared.security;
import java.util.Optional;
/**
* User Lookup Port - Domain-facing interface for resolving user display information.
*
* Allows Bounded Contexts to resolve user IDs to usernames without depending
* on the User Management BC directly.
*
* Implementation: SpringUserLookupAdapter UserRepository
*/
public interface UserLookupPort {
/**
* Resolves a user ID to a username.
*
* @param userId The user's unique identifier
* @return The username, or empty if the user does not exist
*/
Optional<String> resolveUsername(String userId);
}