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

fix(inventory): Audit-Logging, testbare Zeitlogik und defensive Null-Prüfung für blockBatch/unblockBatch

- AuditEvents STOCK_BATCH_BLOCKED/UNBLOCKED hinzugefügt, reason wird als Audit-Details geloggt
- LocalDate.now() aus Stock.unblockBatch() entfernt, referenceDate als Parameter (Application Layer übergibt)
- Defensive Null-Prüfung für expiryDate in unblockBatch MHD-Check
- Ticket 003 erstellt zur Klärung ob reason im Domain-Model persistiert werden soll
This commit is contained in:
Sebastian Frick 2026-02-20 00:17:03 +01:00
parent e7c3258f07
commit d963d7fccc
10 changed files with 112 additions and 33 deletions

View file

@ -1,20 +1,25 @@
package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.BlockStockBatchCommand;
import de.effigenix.application.usermanagement.AuditEvent;
import de.effigenix.application.usermanagement.AuditLogger;
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 BlockStockBatch {
private final StockRepository stockRepository;
private final AuditLogger auditLogger;
public BlockStockBatch(StockRepository stockRepository) {
public BlockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) {
this.stockRepository = stockRepository;
this.auditLogger = auditLogger;
}
public Result<StockError, Void> execute(BlockStockBatchCommand cmd) {
public Result<StockError, Void> execute(BlockStockBatchCommand cmd, ActorId performedBy) {
// 1. Stock laden
Stock stock;
switch (stockRepository.findById(StockId.of(cmd.stockId()))) {
@ -41,6 +46,7 @@ public class BlockStockBatch {
case Result.Success(var ignored) -> { }
}
auditLogger.log(AuditEvent.STOCK_BATCH_BLOCKED, cmd.batchId(), "Reason: " + cmd.reason(), performedBy);
return Result.success(null);
}
}

View file

@ -1,20 +1,27 @@
package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.UnblockStockBatchCommand;
import de.effigenix.application.usermanagement.AuditEvent;
import de.effigenix.application.usermanagement.AuditLogger;
import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Transactional
public class UnblockStockBatch {
private final StockRepository stockRepository;
private final AuditLogger auditLogger;
public UnblockStockBatch(StockRepository stockRepository) {
public UnblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) {
this.stockRepository = stockRepository;
this.auditLogger = auditLogger;
}
public Result<StockError, Void> execute(UnblockStockBatchCommand cmd) {
public Result<StockError, Void> execute(UnblockStockBatchCommand cmd, ActorId performedBy) {
// 1. Stock laden
Stock stock;
switch (stockRepository.findById(StockId.of(cmd.stockId()))) {
@ -29,7 +36,7 @@ public class UnblockStockBatch {
}
// 2. Batch entsperren (Domain validiert)
switch (stock.unblockBatch(StockBatchId.of(cmd.batchId()))) {
switch (stock.unblockBatch(StockBatchId.of(cmd.batchId()), LocalDate.now())) {
case Result.Failure(var err) -> { return Result.failure(err); }
case Result.Success(var ignored) -> { }
}
@ -41,6 +48,7 @@ public class UnblockStockBatch {
case Result.Success(var ignored) -> { }
}
auditLogger.log(AuditEvent.STOCK_BATCH_UNBLOCKED, cmd.batchId(), performedBy);
return Result.success(null);
}
}

View file

@ -46,6 +46,8 @@ public enum AuditEvent {
STOCK_ADJUSTED,
STOCK_MOVEMENT_RECORDED,
INVENTORY_COUNT_PERFORMED,
STOCK_BATCH_BLOCKED,
STOCK_BATCH_UNBLOCKED,
// ==================== Procurement BC ====================
PURCHASE_ORDER_CREATED,

View file

@ -173,7 +173,7 @@ public class Stock {
return Result.success(null);
}
public Result<StockError, Void> unblockBatch(StockBatchId batchId) {
public Result<StockError, Void> unblockBatch(StockBatchId batchId, LocalDate referenceDate) {
StockBatch batch = batches.stream()
.filter(b -> b.id().equals(batchId))
.findFirst()
@ -186,8 +186,8 @@ public class Stock {
}
StockBatchStatus newStatus = StockBatchStatus.AVAILABLE;
if (minimumShelfLife != null) {
LocalDate threshold = LocalDate.now().plusDays(minimumShelfLife.days());
if (minimumShelfLife != null && batch.expiryDate() != null) {
LocalDate threshold = referenceDate.plusDays(minimumShelfLife.days());
if (batch.expiryDate().isBefore(threshold)) {
newStatus = StockBatchStatus.EXPIRING_SOON;
}

View file

@ -10,6 +10,7 @@ import de.effigenix.application.inventory.CreateStorageLocation;
import de.effigenix.application.inventory.DeactivateStorageLocation;
import de.effigenix.application.inventory.ListStorageLocations;
import de.effigenix.application.inventory.UpdateStorageLocation;
import de.effigenix.application.usermanagement.AuditLogger;
import de.effigenix.domain.inventory.StockRepository;
import de.effigenix.domain.inventory.StorageLocationRepository;
import org.springframework.context.annotation.Bean;
@ -63,12 +64,12 @@ public class InventoryUseCaseConfiguration {
}
@Bean
public BlockStockBatch blockStockBatch(StockRepository stockRepository) {
return new BlockStockBatch(stockRepository);
public BlockStockBatch blockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) {
return new BlockStockBatch(stockRepository, auditLogger);
}
@Bean
public UnblockStockBatch unblockStockBatch(StockRepository stockRepository) {
return new UnblockStockBatch(stockRepository);
public UnblockStockBatch unblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) {
return new UnblockStockBatch(stockRepository, auditLogger);
}
}

View file

@ -11,6 +11,7 @@ import de.effigenix.application.inventory.command.CreateStockCommand;
import de.effigenix.application.inventory.command.RemoveStockBatchCommand;
import de.effigenix.application.inventory.command.UnblockStockBatchCommand;
import de.effigenix.domain.inventory.StockError;
import de.effigenix.shared.security.ActorId;
import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest;
import de.effigenix.infrastructure.inventory.web.dto.BlockStockBatchRequest;
import de.effigenix.infrastructure.inventory.web.dto.CreateStockRequest;
@ -136,7 +137,7 @@ public class StockController {
logger.info("Blocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName());
var cmd = new BlockStockBatchCommand(stockId, batchId, request.reason());
var result = blockStockBatch.execute(cmd);
var result = blockStockBatch.execute(cmd, ActorId.of(authentication.getName()));
if (result.isFailure()) {
throw new StockDomainErrorException(result.unsafeGetError());
@ -156,7 +157,7 @@ public class StockController {
logger.info("Unblocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName());
var cmd = new UnblockStockBatchCommand(stockId, batchId);
var result = unblockStockBatch.execute(cmd);
var result = unblockStockBatch.execute(cmd, ActorId.of(authentication.getName()));
if (result.isFailure()) {
throw new StockDomainErrorException(result.unsafeGetError());