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

refactor(inventory): UnitOfWork-Pattern + JdbcClient-Migration

Letzter BC migriert: JPA/Hibernate durch JdbcClient ersetzt,
@Transactional durch UnitOfWork-Pattern in allen schreibenden Use Cases.

- 3 neue Jdbc-Repos: JdbcStorageLocationRepository, JdbcStockMovementRepository,
  JdbcStockRepository (4-Tabellen-Aggregat mit Batches, Reservations, Allocations)
- 20 Use Cases angepasst (UoW für schreibende, @Transactional entfernt für lesende)
- 15 alte JPA-Dateien gelöscht (6 Entities, 3 Mapper, 3 Adapter, 3 Spring Data Repos)
- 9 Unit-Tests mit UoW-Mock-Pattern aktualisiert
This commit is contained in:
Sebastian Frick 2026-02-25 12:57:46 +01:00
parent c89ee359d1
commit d4ac8cb1b9
48 changed files with 1024 additions and 1388 deletions

View file

@ -2,16 +2,17 @@ package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class ActivateStorageLocation { public class ActivateStorageLocation {
private final StorageLocationRepository storageLocationRepository; private final StorageLocationRepository storageLocationRepository;
private final UnitOfWork unitOfWork;
public ActivateStorageLocation(StorageLocationRepository storageLocationRepository) { public ActivateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) {
this.storageLocationRepository = storageLocationRepository; this.storageLocationRepository = storageLocationRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StorageLocationError, StorageLocation> execute(String storageLocationId, ActorId performedBy) { public Result<StorageLocationError, StorageLocation> execute(String storageLocationId, ActorId performedBy) {
@ -36,12 +37,13 @@ public class ActivateStorageLocation {
} }
// 3. Speichern // 3. Speichern
switch (storageLocationRepository.save(location)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (storageLocationRepository.save(location)) {
{ return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(location); return Result.success(location);
});
} }
} }

View file

@ -3,15 +3,16 @@ package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.AddStockBatchCommand; import de.effigenix.application.inventory.command.AddStockBatchCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional; import de.effigenix.shared.persistence.UnitOfWork;
@Transactional
public class AddStockBatch { public class AddStockBatch {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final UnitOfWork unitOfWork;
public AddStockBatch(StockRepository stockRepository) { public AddStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, StockBatch> execute(AddStockBatchCommand cmd) { public Result<StockError, StockBatch> execute(AddStockBatchCommand cmd) {
@ -42,12 +43,13 @@ public class AddStockBatch {
} }
// 3. Stock speichern // 3. Stock speichern
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockRepository.save(stock)) {
{ return Result.failure(new StockError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(batch); return Result.success(batch);
});
} }
} }

View file

@ -5,18 +5,19 @@ import de.effigenix.application.usermanagement.AuditEvent;
import de.effigenix.application.usermanagement.AuditLogger; import de.effigenix.application.usermanagement.AuditLogger;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class BlockStockBatch { public class BlockStockBatch {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final AuditLogger auditLogger; private final AuditLogger auditLogger;
private final UnitOfWork unitOfWork;
public BlockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) { public BlockStockBatch(StockRepository stockRepository, AuditLogger auditLogger, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.auditLogger = auditLogger; this.auditLogger = auditLogger;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, Void> execute(BlockStockBatchCommand cmd, ActorId performedBy) { public Result<StockError, Void> execute(BlockStockBatchCommand cmd, ActorId performedBy) {
@ -40,13 +41,14 @@ public class BlockStockBatch {
} }
// 3. Stock speichern // 3. Stock speichern
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockRepository.save(stock)) {
{ return Result.failure(new StockError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
auditLogger.log(AuditEvent.STOCK_BATCH_BLOCKED, cmd.batchId(), "Reason: " + cmd.reason(), performedBy); auditLogger.log(AuditEvent.STOCK_BATCH_BLOCKED, cmd.batchId(), "Reason: " + cmd.reason(), performedBy);
return Result.success(null); return Result.success(null);
});
} }
} }

View file

@ -4,15 +4,16 @@ import de.effigenix.application.inventory.command.CreateStockCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional; import de.effigenix.shared.persistence.UnitOfWork;
@Transactional
public class CreateStock { public class CreateStock {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final UnitOfWork unitOfWork;
public CreateStock(StockRepository stockRepository) { public CreateStock(StockRepository stockRepository, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, Stock> execute(CreateStockCommand cmd) { public Result<StockError, Stock> execute(CreateStockCommand cmd) {
@ -43,17 +44,18 @@ public class CreateStock {
} }
// 4. Speichern (Race-Condition-Schutz: DuplicateEntry DuplicateStock) // 4. Speichern (Race-Condition-Schutz: DuplicateEntry DuplicateStock)
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> { switch (stockRepository.save(stock)) {
if (err instanceof RepositoryError.DuplicateEntry) { case Result.Failure(var err) -> {
return Result.failure(new StockError.DuplicateStock( if (err instanceof RepositoryError.DuplicateEntry) {
cmd.articleId(), cmd.storageLocationId())); return Result.failure(new StockError.DuplicateStock(
cmd.articleId(), cmd.storageLocationId()));
}
return Result.failure(new StockError.RepositoryFailure(err.message()));
} }
return Result.failure(new StockError.RepositoryFailure(err.message())); case Result.Success(var ignored) -> { }
} }
case Result.Success(var ignored) -> { } return Result.success(stock);
} });
return Result.success(stock);
} }
} }

View file

@ -3,16 +3,17 @@ package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.CreateStorageLocationCommand; import de.effigenix.application.inventory.command.CreateStorageLocationCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class CreateStorageLocation { public class CreateStorageLocation {
private final StorageLocationRepository storageLocationRepository; private final StorageLocationRepository storageLocationRepository;
private final UnitOfWork unitOfWork;
public CreateStorageLocation(StorageLocationRepository storageLocationRepository) { public CreateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) {
this.storageLocationRepository = storageLocationRepository; this.storageLocationRepository = storageLocationRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StorageLocationError, StorageLocation> execute(CreateStorageLocationCommand cmd, ActorId performedBy) { public Result<StorageLocationError, StorageLocation> execute(CreateStorageLocationCommand cmd, ActorId performedBy) {
@ -40,12 +41,13 @@ public class CreateStorageLocation {
} }
// 4. Speichern // 4. Speichern
switch (storageLocationRepository.save(location)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (storageLocationRepository.save(location)) {
{ return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(location); return Result.success(location);
});
} }
} }

View file

@ -2,18 +2,19 @@ package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class DeactivateStorageLocation { public class DeactivateStorageLocation {
private final StorageLocationRepository storageLocationRepository; private final StorageLocationRepository storageLocationRepository;
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final UnitOfWork unitOfWork;
public DeactivateStorageLocation(StorageLocationRepository storageLocationRepository, StockRepository stockRepository) { public DeactivateStorageLocation(StorageLocationRepository storageLocationRepository, StockRepository stockRepository, UnitOfWork unitOfWork) {
this.storageLocationRepository = storageLocationRepository; this.storageLocationRepository = storageLocationRepository;
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StorageLocationError, StorageLocation> execute(String storageLocationId, ActorId performedBy) { public Result<StorageLocationError, StorageLocation> execute(String storageLocationId, ActorId performedBy) {
@ -49,12 +50,13 @@ public class DeactivateStorageLocation {
} }
// 4. Speichern // 4. Speichern
switch (storageLocationRepository.save(location)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (storageLocationRepository.save(location)) {
{ return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(location); return Result.success(location);
});
} }
} }

View file

@ -5,9 +5,7 @@ import de.effigenix.domain.inventory.StockError;
import de.effigenix.domain.inventory.StockId; import de.effigenix.domain.inventory.StockId;
import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StockRepository;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional;
@Transactional(readOnly = true)
public class GetStock { public class GetStock {
private final StockRepository stockRepository; private final StockRepository stockRepository;

View file

@ -8,9 +8,7 @@ import de.effigenix.domain.inventory.StockMovementRepository;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import org.springframework.transaction.annotation.Transactional;
@Transactional(readOnly = true)
public class GetStockMovement { public class GetStockMovement {
private final StockMovementRepository stockMovementRepository; private final StockMovementRepository stockMovementRepository;

View file

@ -8,9 +8,7 @@ import de.effigenix.domain.inventory.StorageLocationRepository;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import org.springframework.transaction.annotation.Transactional;
@Transactional(readOnly = true)
public class GetStorageLocation { public class GetStorageLocation {
private final StorageLocationRepository storageLocationRepository; private final StorageLocationRepository storageLocationRepository;

View file

@ -11,12 +11,10 @@ import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
@Transactional(readOnly = true)
public class ListStockMovements { public class ListStockMovements {
private final StockMovementRepository stockMovementRepository; private final StockMovementRepository stockMovementRepository;

View file

@ -7,11 +7,9 @@ import de.effigenix.domain.inventory.StorageLocationId;
import de.effigenix.domain.masterdata.ArticleId; import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
@Transactional(readOnly = true)
public class ListStocks { public class ListStocks {
private final StockRepository stockRepository; private final StockRepository stockRepository;

View file

@ -7,7 +7,6 @@ import de.effigenix.domain.inventory.StockRepository;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
@ -21,7 +20,6 @@ public class ListStocksBelowMinimum {
this.authPort = authPort; this.authPort = authPort;
} }
@Transactional(readOnly = true)
public Result<StockError, List<Stock>> execute(ActorId performedBy) { public Result<StockError, List<Stock>> execute(ActorId performedBy) {
if (!authPort.can(performedBy, InventoryAction.STOCK_READ)) { if (!authPort.can(performedBy, InventoryAction.STOCK_READ)) {
return Result.failure(new StockError.Unauthorized("Not authorized to list stocks below minimum")); return Result.failure(new StockError.Unauthorized("Not authorized to list stocks below minimum"));

View file

@ -3,11 +3,9 @@ package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
@Transactional(readOnly = true)
public class ListStorageLocations { public class ListStorageLocations {
private final StorageLocationRepository storageLocationRepository; private final StorageLocationRepository storageLocationRepository;

View file

@ -7,19 +7,20 @@ import de.effigenix.domain.inventory.StockMovementDraft;
import de.effigenix.domain.inventory.StockMovementError; import de.effigenix.domain.inventory.StockMovementError;
import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.domain.inventory.StockMovementRepository;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class RecordStockMovement { public class RecordStockMovement {
private final StockMovementRepository stockMovementRepository; private final StockMovementRepository stockMovementRepository;
private final AuthorizationPort authPort; private final AuthorizationPort authPort;
private final UnitOfWork unitOfWork;
public RecordStockMovement(StockMovementRepository stockMovementRepository, AuthorizationPort authPort) { public RecordStockMovement(StockMovementRepository stockMovementRepository, AuthorizationPort authPort, UnitOfWork unitOfWork) {
this.stockMovementRepository = stockMovementRepository; this.stockMovementRepository = stockMovementRepository;
this.authPort = authPort; this.authPort = authPort;
this.unitOfWork = unitOfWork;
} }
public Result<StockMovementError, StockMovement> execute(RecordStockMovementCommand cmd, ActorId performedBy) { public Result<StockMovementError, StockMovement> execute(RecordStockMovementCommand cmd, ActorId performedBy) {
@ -42,12 +43,13 @@ public class RecordStockMovement {
case Result.Success(var val) -> movement = val; case Result.Success(var val) -> movement = val;
} }
switch (stockMovementRepository.save(movement)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockMovementRepository.save(movement)) {
{ return Result.failure(new StockMovementError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockMovementError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(movement); return Result.success(movement);
});
} }
} }

View file

@ -3,15 +3,16 @@ package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.ReleaseReservationCommand; import de.effigenix.application.inventory.command.ReleaseReservationCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional; import de.effigenix.shared.persistence.UnitOfWork;
@Transactional
public class ReleaseReservation { public class ReleaseReservation {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final UnitOfWork unitOfWork;
public ReleaseReservation(StockRepository stockRepository) { public ReleaseReservation(StockRepository stockRepository, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, Void> execute(ReleaseReservationCommand cmd) { public Result<StockError, Void> execute(ReleaseReservationCommand cmd) {
@ -35,12 +36,13 @@ public class ReleaseReservation {
} }
// 3. Speichern // 3. Speichern
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockRepository.save(stock)) {
{ return Result.failure(new StockError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(null); return Result.success(null);
});
} }
} }

View file

@ -5,17 +5,18 @@ import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import org.springframework.transaction.annotation.Transactional; import de.effigenix.shared.persistence.UnitOfWork;
import java.math.BigDecimal; import java.math.BigDecimal;
@Transactional
public class RemoveStockBatch { public class RemoveStockBatch {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final UnitOfWork unitOfWork;
public RemoveStockBatch(StockRepository stockRepository) { public RemoveStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, Void> execute(RemoveStockBatchCommand cmd) { public Result<StockError, Void> execute(RemoveStockBatchCommand cmd) {
@ -59,12 +60,13 @@ public class RemoveStockBatch {
} }
// 4. Stock speichern // 4. Stock speichern
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockRepository.save(stock)) {
{ return Result.failure(new StockError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(null); return Result.success(null);
});
} }
} }

View file

@ -3,15 +3,16 @@ package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.ReserveStockCommand; import de.effigenix.application.inventory.command.ReserveStockCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional; import de.effigenix.shared.persistence.UnitOfWork;
@Transactional
public class ReserveStock { public class ReserveStock {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final UnitOfWork unitOfWork;
public ReserveStock(StockRepository stockRepository) { public ReserveStock(StockRepository stockRepository, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, Reservation> execute(ReserveStockCommand cmd) { public Result<StockError, Reservation> execute(ReserveStockCommand cmd) {
@ -47,12 +48,13 @@ public class ReserveStock {
} }
// 4. Speichern // 4. Speichern
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockRepository.save(stock)) {
{ return Result.failure(new StockError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(reservation); return Result.success(reservation);
});
} }
} }

View file

@ -5,20 +5,21 @@ import de.effigenix.application.usermanagement.AuditEvent;
import de.effigenix.application.usermanagement.AuditLogger; import de.effigenix.application.usermanagement.AuditLogger;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate; import java.time.LocalDate;
@Transactional
public class UnblockStockBatch { public class UnblockStockBatch {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final AuditLogger auditLogger; private final AuditLogger auditLogger;
private final UnitOfWork unitOfWork;
public UnblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) { public UnblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.auditLogger = auditLogger; this.auditLogger = auditLogger;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, Void> execute(UnblockStockBatchCommand cmd, ActorId performedBy) { public Result<StockError, Void> execute(UnblockStockBatchCommand cmd, ActorId performedBy) {
@ -42,13 +43,14 @@ public class UnblockStockBatch {
} }
// 3. Stock speichern // 3. Stock speichern
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockRepository.save(stock)) {
{ return Result.failure(new StockError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
auditLogger.log(AuditEvent.STOCK_BATCH_UNBLOCKED, cmd.batchId(), performedBy); auditLogger.log(AuditEvent.STOCK_BATCH_UNBLOCKED, cmd.batchId(), performedBy);
return Result.success(null); return Result.success(null);
});
} }
} }

View file

@ -3,15 +3,16 @@ package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.UpdateStockCommand; import de.effigenix.application.inventory.command.UpdateStockCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional; import de.effigenix.shared.persistence.UnitOfWork;
@Transactional
public class UpdateStock { public class UpdateStock {
private final StockRepository stockRepository; private final StockRepository stockRepository;
private final UnitOfWork unitOfWork;
public UpdateStock(StockRepository stockRepository) { public UpdateStock(StockRepository stockRepository, UnitOfWork unitOfWork) {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StockError, Stock> execute(UpdateStockCommand cmd) { public Result<StockError, Stock> execute(UpdateStockCommand cmd) {
@ -46,12 +47,13 @@ public class UpdateStock {
} }
// 3. Speichern // 3. Speichern
switch (stockRepository.save(stock)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (stockRepository.save(stock)) {
{ return Result.failure(new StockError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StockError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(stock); return Result.success(stock);
});
} }
} }

View file

@ -3,16 +3,17 @@ package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.UpdateStorageLocationCommand; import de.effigenix.application.inventory.command.UpdateStorageLocationCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class UpdateStorageLocation { public class UpdateStorageLocation {
private final StorageLocationRepository storageLocationRepository; private final StorageLocationRepository storageLocationRepository;
private final UnitOfWork unitOfWork;
public UpdateStorageLocation(StorageLocationRepository storageLocationRepository) { public UpdateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) {
this.storageLocationRepository = storageLocationRepository; this.storageLocationRepository = storageLocationRepository;
this.unitOfWork = unitOfWork;
} }
public Result<StorageLocationError, StorageLocation> execute(UpdateStorageLocationCommand cmd, ActorId performedBy) { public Result<StorageLocationError, StorageLocation> execute(UpdateStorageLocationCommand cmd, ActorId performedBy) {
@ -51,12 +52,13 @@ public class UpdateStorageLocation {
} }
// 4. Speichern // 4. Speichern
switch (storageLocationRepository.save(location)) { return unitOfWork.executeAtomically(() -> {
case Result.Failure(var err) -> switch (storageLocationRepository.save(location)) {
{ return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } case Result.Failure(var err) ->
case Result.Success(var ignored) -> { } { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); }
} case Result.Success(var ignored) -> { }
}
return Result.success(location); return Result.success(location);
});
} }
} }

View file

@ -25,6 +25,7 @@ import de.effigenix.application.usermanagement.AuditLogger;
import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.domain.inventory.StockMovementRepository;
import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StockRepository;
import de.effigenix.domain.inventory.StorageLocationRepository; import de.effigenix.domain.inventory.StorageLocationRepository;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -35,23 +36,23 @@ public class InventoryUseCaseConfiguration {
// ==================== StorageLocation Use Cases ==================== // ==================== StorageLocation Use Cases ====================
@Bean @Bean
public CreateStorageLocation createStorageLocation(StorageLocationRepository storageLocationRepository) { public CreateStorageLocation createStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) {
return new CreateStorageLocation(storageLocationRepository); return new CreateStorageLocation(storageLocationRepository, unitOfWork);
} }
@Bean @Bean
public UpdateStorageLocation updateStorageLocation(StorageLocationRepository storageLocationRepository) { public UpdateStorageLocation updateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) {
return new UpdateStorageLocation(storageLocationRepository); return new UpdateStorageLocation(storageLocationRepository, unitOfWork);
} }
@Bean @Bean
public DeactivateStorageLocation deactivateStorageLocation(StorageLocationRepository storageLocationRepository, StockRepository stockRepository) { public DeactivateStorageLocation deactivateStorageLocation(StorageLocationRepository storageLocationRepository, StockRepository stockRepository, UnitOfWork unitOfWork) {
return new DeactivateStorageLocation(storageLocationRepository, stockRepository); return new DeactivateStorageLocation(storageLocationRepository, stockRepository, unitOfWork);
} }
@Bean @Bean
public ActivateStorageLocation activateStorageLocation(StorageLocationRepository storageLocationRepository) { public ActivateStorageLocation activateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) {
return new ActivateStorageLocation(storageLocationRepository); return new ActivateStorageLocation(storageLocationRepository, unitOfWork);
} }
@Bean @Bean
@ -67,13 +68,13 @@ public class InventoryUseCaseConfiguration {
// ==================== Stock Use Cases ==================== // ==================== Stock Use Cases ====================
@Bean @Bean
public CreateStock createStock(StockRepository stockRepository) { public CreateStock createStock(StockRepository stockRepository, UnitOfWork unitOfWork) {
return new CreateStock(stockRepository); return new CreateStock(stockRepository, unitOfWork);
} }
@Bean @Bean
public UpdateStock updateStock(StockRepository stockRepository) { public UpdateStock updateStock(StockRepository stockRepository, UnitOfWork unitOfWork) {
return new UpdateStock(stockRepository); return new UpdateStock(stockRepository, unitOfWork);
} }
@Bean @Bean
@ -87,33 +88,33 @@ public class InventoryUseCaseConfiguration {
} }
@Bean @Bean
public AddStockBatch addStockBatch(StockRepository stockRepository) { public AddStockBatch addStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) {
return new AddStockBatch(stockRepository); return new AddStockBatch(stockRepository, unitOfWork);
} }
@Bean @Bean
public RemoveStockBatch removeStockBatch(StockRepository stockRepository) { public RemoveStockBatch removeStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) {
return new RemoveStockBatch(stockRepository); return new RemoveStockBatch(stockRepository, unitOfWork);
} }
@Bean @Bean
public BlockStockBatch blockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) { public BlockStockBatch blockStockBatch(StockRepository stockRepository, AuditLogger auditLogger, UnitOfWork unitOfWork) {
return new BlockStockBatch(stockRepository, auditLogger); return new BlockStockBatch(stockRepository, auditLogger, unitOfWork);
} }
@Bean @Bean
public UnblockStockBatch unblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) { public UnblockStockBatch unblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger, UnitOfWork unitOfWork) {
return new UnblockStockBatch(stockRepository, auditLogger); return new UnblockStockBatch(stockRepository, auditLogger, unitOfWork);
} }
@Bean @Bean
public ReserveStock reserveStock(StockRepository stockRepository) { public ReserveStock reserveStock(StockRepository stockRepository, UnitOfWork unitOfWork) {
return new ReserveStock(stockRepository); return new ReserveStock(stockRepository, unitOfWork);
} }
@Bean @Bean
public ReleaseReservation releaseReservation(StockRepository stockRepository) { public ReleaseReservation releaseReservation(StockRepository stockRepository, UnitOfWork unitOfWork) {
return new ReleaseReservation(stockRepository); return new ReleaseReservation(stockRepository, unitOfWork);
} }
@Bean @Bean
@ -129,8 +130,8 @@ public class InventoryUseCaseConfiguration {
// ==================== StockMovement Use Cases ==================== // ==================== StockMovement Use Cases ====================
@Bean @Bean
public RecordStockMovement recordStockMovement(StockMovementRepository stockMovementRepository, AuthorizationPort authorizationPort) { public RecordStockMovement recordStockMovement(StockMovementRepository stockMovementRepository, AuthorizationPort authorizationPort, UnitOfWork unitOfWork) {
return new RecordStockMovement(stockMovementRepository, authorizationPort); return new RecordStockMovement(stockMovementRepository, authorizationPort, unitOfWork);
} }
@Bean @Bean

View file

@ -1,76 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "reservations")
public class ReservationEntity {
@Id
@Column(name = "id", nullable = false, length = 36)
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stock_id", nullable = false)
private StockEntity stock;
@Column(name = "reference_type", nullable = false, length = 30)
private String referenceType;
@Column(name = "reference_id", nullable = false, length = 100)
private String referenceId;
@Column(name = "quantity_amount", nullable = false, precision = 19, scale = 6)
private BigDecimal quantityAmount;
@Column(name = "quantity_unit", nullable = false, length = 20)
private String quantityUnit;
@Column(name = "priority", nullable = false, length = 20)
private String priority;
@Column(name = "reserved_at", nullable = false)
private Instant reservedAt;
@OneToMany(mappedBy = "reservation", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<StockBatchAllocationEntity> allocations = new ArrayList<>();
protected ReservationEntity() {}
public ReservationEntity(String id, StockEntity stock, String referenceType, String referenceId,
BigDecimal quantityAmount, String quantityUnit, String priority,
Instant reservedAt) {
this.id = id;
this.stock = stock;
this.referenceType = referenceType;
this.referenceId = referenceId;
this.quantityAmount = quantityAmount;
this.quantityUnit = quantityUnit;
this.priority = priority;
this.reservedAt = reservedAt;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public StockEntity getStock() { return stock; }
public void setStock(StockEntity stock) { this.stock = stock; }
public String getReferenceType() { return referenceType; }
public void setReferenceType(String referenceType) { this.referenceType = referenceType; }
public String getReferenceId() { return referenceId; }
public void setReferenceId(String referenceId) { this.referenceId = referenceId; }
public BigDecimal getQuantityAmount() { return quantityAmount; }
public void setQuantityAmount(BigDecimal quantityAmount) { this.quantityAmount = quantityAmount; }
public String getQuantityUnit() { return quantityUnit; }
public void setQuantityUnit(String quantityUnit) { this.quantityUnit = quantityUnit; }
public String getPriority() { return priority; }
public void setPriority(String priority) { this.priority = priority; }
public Instant getReservedAt() { return reservedAt; }
public void setReservedAt(Instant reservedAt) { this.reservedAt = reservedAt; }
public List<StockBatchAllocationEntity> getAllocations() { return allocations; }
public void setAllocations(List<StockBatchAllocationEntity> allocations) { this.allocations = allocations; }
}

View file

@ -1,49 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "stock_batch_allocations")
public class StockBatchAllocationEntity {
@Id
@Column(name = "id", nullable = false, length = 36)
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reservation_id", nullable = false)
private ReservationEntity reservation;
@Column(name = "stock_batch_id", nullable = false, length = 36)
private String stockBatchId;
@Column(name = "allocated_quantity_amount", nullable = false, precision = 19, scale = 6)
private BigDecimal allocatedQuantityAmount;
@Column(name = "allocated_quantity_unit", nullable = false, length = 20)
private String allocatedQuantityUnit;
protected StockBatchAllocationEntity() {}
public StockBatchAllocationEntity(String id, ReservationEntity reservation, String stockBatchId,
BigDecimal allocatedQuantityAmount, String allocatedQuantityUnit) {
this.id = id;
this.reservation = reservation;
this.stockBatchId = stockBatchId;
this.allocatedQuantityAmount = allocatedQuantityAmount;
this.allocatedQuantityUnit = allocatedQuantityUnit;
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public ReservationEntity getReservation() { return reservation; }
public void setReservation(ReservationEntity reservation) { this.reservation = reservation; }
public String getStockBatchId() { return stockBatchId; }
public void setStockBatchId(String stockBatchId) { this.stockBatchId = stockBatchId; }
public BigDecimal getAllocatedQuantityAmount() { return allocatedQuantityAmount; }
public void setAllocatedQuantityAmount(BigDecimal allocatedQuantityAmount) { this.allocatedQuantityAmount = allocatedQuantityAmount; }
public String getAllocatedQuantityUnit() { return allocatedQuantityUnit; }
public void setAllocatedQuantityUnit(String allocatedQuantityUnit) { this.allocatedQuantityUnit = allocatedQuantityUnit; }
}

View file

@ -1,77 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
@Entity
@Table(name = "stock_batches")
public class StockBatchEntity {
@Id
@Column(name = "id", nullable = false, length = 36)
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stock_id", nullable = false)
private StockEntity stock;
@Column(name = "batch_id", nullable = false, length = 100)
private String batchId;
@Column(name = "batch_type", nullable = false, length = 20)
private String batchType;
@Column(name = "quantity_amount", nullable = false, precision = 19, scale = 6)
private BigDecimal quantityAmount;
@Column(name = "quantity_unit", nullable = false, length = 20)
private String quantityUnit;
@Column(name = "expiry_date", nullable = false)
private LocalDate expiryDate;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "received_at", nullable = false)
private Instant receivedAt;
protected StockBatchEntity() {}
public StockBatchEntity(String id, StockEntity stock, String batchId, String batchType,
BigDecimal quantityAmount, String quantityUnit, LocalDate expiryDate,
String status, Instant receivedAt) {
this.id = id;
this.stock = stock;
this.batchId = batchId;
this.batchType = batchType;
this.quantityAmount = quantityAmount;
this.quantityUnit = quantityUnit;
this.expiryDate = expiryDate;
this.status = status;
this.receivedAt = receivedAt;
}
public String getId() { return id; }
public StockEntity getStock() { return stock; }
public String getBatchId() { return batchId; }
public String getBatchType() { return batchType; }
public BigDecimal getQuantityAmount() { return quantityAmount; }
public String getQuantityUnit() { return quantityUnit; }
public LocalDate getExpiryDate() { return expiryDate; }
public String getStatus() { return status; }
public Instant getReceivedAt() { return receivedAt; }
public void setId(String id) { this.id = id; }
public void setStock(StockEntity stock) { this.stock = stock; }
public void setBatchId(String batchId) { this.batchId = batchId; }
public void setBatchType(String batchType) { this.batchType = batchType; }
public void setQuantityAmount(BigDecimal quantityAmount) { this.quantityAmount = quantityAmount; }
public void setQuantityUnit(String quantityUnit) { this.quantityUnit = quantityUnit; }
public void setExpiryDate(LocalDate expiryDate) { this.expiryDate = expiryDate; }
public void setStatus(String status) { this.status = status; }
public void setReceivedAt(Instant receivedAt) { this.receivedAt = receivedAt; }
}

View file

@ -1,65 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "stocks")
public class StockEntity {
@Id
@Column(name = "id", nullable = false, length = 36)
private String id;
@Column(name = "article_id", nullable = false, length = 36)
private String articleId;
@Column(name = "storage_location_id", nullable = false, length = 36)
private String storageLocationId;
@Column(name = "minimum_level_amount", precision = 12, scale = 3)
private BigDecimal minimumLevelAmount;
@Column(name = "minimum_level_unit", length = 20)
private String minimumLevelUnit;
@Column(name = "minimum_shelf_life_days")
private Integer minimumShelfLifeDays;
@OneToMany(mappedBy = "stock", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<StockBatchEntity> batches = new ArrayList<>();
@OneToMany(mappedBy = "stock", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<ReservationEntity> reservations = new ArrayList<>();
public StockEntity() {}
// ==================== Getters & Setters ====================
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getArticleId() { return articleId; }
public void setArticleId(String articleId) { this.articleId = articleId; }
public String getStorageLocationId() { return storageLocationId; }
public void setStorageLocationId(String storageLocationId) { this.storageLocationId = storageLocationId; }
public BigDecimal getMinimumLevelAmount() { return minimumLevelAmount; }
public void setMinimumLevelAmount(BigDecimal minimumLevelAmount) { this.minimumLevelAmount = minimumLevelAmount; }
public String getMinimumLevelUnit() { return minimumLevelUnit; }
public void setMinimumLevelUnit(String minimumLevelUnit) { this.minimumLevelUnit = minimumLevelUnit; }
public Integer getMinimumShelfLifeDays() { return minimumShelfLifeDays; }
public void setMinimumShelfLifeDays(Integer minimumShelfLifeDays) { this.minimumShelfLifeDays = minimumShelfLifeDays; }
public List<StockBatchEntity> getBatches() { return batches; }
public void setBatches(List<StockBatchEntity> batches) { this.batches = batches; }
public List<ReservationEntity> getReservations() { return reservations; }
public void setReservations(List<ReservationEntity> reservations) { this.reservations = reservations; }
}

View file

@ -1,120 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
@Entity
@Table(name = "stock_movements")
public class StockMovementEntity {
@Id
@Column(name = "id", nullable = false, length = 36)
private String id;
@Column(name = "stock_id", nullable = false, length = 36)
private String stockId;
@Column(name = "article_id", nullable = false, length = 36)
private String articleId;
@Column(name = "stock_batch_id", nullable = false, length = 36)
private String stockBatchId;
@Column(name = "batch_id", nullable = false, length = 100)
private String batchId;
@Column(name = "batch_type", nullable = false, length = 20)
private String batchType;
@Column(name = "movement_type", nullable = false, length = 30)
private String movementType;
@Column(name = "direction", nullable = false, length = 10)
private String direction;
@Column(name = "quantity_amount", nullable = false, precision = 19, scale = 6)
private BigDecimal quantityAmount;
@Column(name = "quantity_unit", nullable = false, length = 20)
private String quantityUnit;
@Column(name = "reason", length = 500)
private String reason;
@Column(name = "reference_document_id", length = 100)
private String referenceDocumentId;
@Column(name = "performed_by", nullable = false, length = 36)
private String performedBy;
@Column(name = "performed_at", nullable = false)
private Instant performedAt;
public StockMovementEntity() {}
public StockMovementEntity(String id, String stockId, String articleId, String stockBatchId,
String batchId, String batchType, String movementType, String direction,
BigDecimal quantityAmount, String quantityUnit, String reason,
String referenceDocumentId, String performedBy, Instant performedAt) {
this.id = id;
this.stockId = stockId;
this.articleId = articleId;
this.stockBatchId = stockBatchId;
this.batchId = batchId;
this.batchType = batchType;
this.movementType = movementType;
this.direction = direction;
this.quantityAmount = quantityAmount;
this.quantityUnit = quantityUnit;
this.reason = reason;
this.referenceDocumentId = referenceDocumentId;
this.performedBy = performedBy;
this.performedAt = performedAt;
}
// ==================== Getters & Setters ====================
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getStockId() { return stockId; }
public void setStockId(String stockId) { this.stockId = stockId; }
public String getArticleId() { return articleId; }
public void setArticleId(String articleId) { this.articleId = articleId; }
public String getStockBatchId() { return stockBatchId; }
public void setStockBatchId(String stockBatchId) { this.stockBatchId = stockBatchId; }
public String getBatchId() { return batchId; }
public void setBatchId(String batchId) { this.batchId = batchId; }
public String getBatchType() { return batchType; }
public void setBatchType(String batchType) { this.batchType = batchType; }
public String getMovementType() { return movementType; }
public void setMovementType(String movementType) { this.movementType = movementType; }
public String getDirection() { return direction; }
public void setDirection(String direction) { this.direction = direction; }
public BigDecimal getQuantityAmount() { return quantityAmount; }
public void setQuantityAmount(BigDecimal quantityAmount) { this.quantityAmount = quantityAmount; }
public String getQuantityUnit() { return quantityUnit; }
public void setQuantityUnit(String quantityUnit) { this.quantityUnit = quantityUnit; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public String getReferenceDocumentId() { return referenceDocumentId; }
public void setReferenceDocumentId(String referenceDocumentId) { this.referenceDocumentId = referenceDocumentId; }
public String getPerformedBy() { return performedBy; }
public void setPerformedBy(String performedBy) { this.performedBy = performedBy; }
public Instant getPerformedAt() { return performedAt; }
public void setPerformedAt(Instant performedAt) { this.performedAt = performedAt; }
}

View file

@ -1,51 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "storage_locations")
public class StorageLocationEntity {
@Id
@Column(name = "id", nullable = false, length = 36)
private String id;
@Column(name = "name", nullable = false, unique = true, length = 100)
private String name;
@Column(name = "storage_type", nullable = false, length = 30)
private String storageType;
@Column(name = "min_temperature", precision = 5, scale = 1)
private BigDecimal minTemperature;
@Column(name = "max_temperature", precision = 5, scale = 1)
private BigDecimal maxTemperature;
@Column(name = "active", nullable = false)
private boolean active;
public StorageLocationEntity() {}
// ==================== Getters & Setters ====================
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getStorageType() { return storageType; }
public void setStorageType(String storageType) { this.storageType = storageType; }
public BigDecimal getMinTemperature() { return minTemperature; }
public void setMinTemperature(BigDecimal minTemperature) { this.minTemperature = minTemperature; }
public BigDecimal getMaxTemperature() { return maxTemperature; }
public void setMaxTemperature(BigDecimal maxTemperature) { this.maxTemperature = maxTemperature; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}

View file

@ -1,168 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.mapper;
import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.infrastructure.inventory.persistence.entity.ReservationEntity;
import de.effigenix.infrastructure.inventory.persistence.entity.StockBatchAllocationEntity;
import de.effigenix.infrastructure.inventory.persistence.entity.StockBatchEntity;
import de.effigenix.infrastructure.inventory.persistence.entity.StockEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class StockMapper {
public StockEntity toEntity(Stock stock) {
var entity = new StockEntity();
entity.setId(stock.id().value());
entity.setArticleId(stock.articleId().value());
entity.setStorageLocationId(stock.storageLocationId().value());
if (stock.minimumLevel() != null) {
entity.setMinimumLevelAmount(stock.minimumLevel().quantity().amount());
entity.setMinimumLevelUnit(stock.minimumLevel().quantity().uom().name());
}
if (stock.minimumShelfLife() != null) {
entity.setMinimumShelfLifeDays(stock.minimumShelfLife().days());
}
List<StockBatchEntity> batchEntities = stock.batches().stream()
.map(b -> toBatchEntity(b, entity))
.collect(Collectors.toList());
entity.setBatches(batchEntities);
List<ReservationEntity> reservationEntities = stock.reservations().stream()
.map(r -> toReservationEntity(r, entity))
.collect(Collectors.toList());
entity.setReservations(reservationEntities);
return entity;
}
public Stock toDomain(StockEntity entity) {
MinimumLevel minimumLevel = null;
if (entity.getMinimumLevelAmount() != null && entity.getMinimumLevelUnit() != null) {
var quantity = Quantity.reconstitute(
entity.getMinimumLevelAmount(),
UnitOfMeasure.valueOf(entity.getMinimumLevelUnit())
);
minimumLevel = new MinimumLevel(quantity);
}
MinimumShelfLife minimumShelfLife = null;
if (entity.getMinimumShelfLifeDays() != null) {
minimumShelfLife = new MinimumShelfLife(entity.getMinimumShelfLifeDays());
}
List<StockBatch> batches = entity.getBatches().stream()
.map(this::toDomainBatch)
.collect(Collectors.toList());
List<Reservation> reservations = entity.getReservations().stream()
.map(this::toDomainReservation)
.collect(Collectors.toList());
return Stock.reconstitute(
StockId.of(entity.getId()),
ArticleId.of(entity.getArticleId()),
StorageLocationId.of(entity.getStorageLocationId()),
minimumLevel,
minimumShelfLife,
batches,
reservations
);
}
private StockBatchEntity toBatchEntity(StockBatch batch, StockEntity stockEntity) {
return new StockBatchEntity(
batch.id().value(),
stockEntity,
batch.batchReference().batchId(),
batch.batchReference().batchType().name(),
batch.quantity().amount(),
batch.quantity().uom().name(),
batch.expiryDate(),
batch.status().name(),
batch.receivedAt()
);
}
private StockBatch toDomainBatch(StockBatchEntity entity) {
return StockBatch.reconstitute(
StockBatchId.of(entity.getId()),
new BatchReference(entity.getBatchId(), BatchType.valueOf(entity.getBatchType())),
Quantity.reconstitute(
entity.getQuantityAmount(),
UnitOfMeasure.valueOf(entity.getQuantityUnit())
),
entity.getExpiryDate(),
StockBatchStatus.valueOf(entity.getStatus()),
entity.getReceivedAt()
);
}
private ReservationEntity toReservationEntity(Reservation reservation, StockEntity stockEntity) {
var entity = new ReservationEntity(
reservation.id().value(),
stockEntity,
reservation.referenceType().name(),
reservation.referenceId(),
reservation.quantity().amount(),
reservation.quantity().uom().name(),
reservation.priority().name(),
reservation.reservedAt()
);
List<StockBatchAllocationEntity> allocationEntities = reservation.allocations().stream()
.map(a -> toAllocationEntity(a, entity))
.collect(Collectors.toList());
entity.setAllocations(allocationEntities);
return entity;
}
private StockBatchAllocationEntity toAllocationEntity(StockBatchAllocation allocation, ReservationEntity reservationEntity) {
return new StockBatchAllocationEntity(
allocation.id().value(),
reservationEntity,
allocation.stockBatchId().value(),
allocation.allocatedQuantity().amount(),
allocation.allocatedQuantity().uom().name()
);
}
private Reservation toDomainReservation(ReservationEntity entity) {
List<StockBatchAllocation> allocations = entity.getAllocations().stream()
.map(this::toDomainAllocation)
.collect(Collectors.toList());
return new Reservation(
ReservationId.of(entity.getId()),
ReferenceType.valueOf(entity.getReferenceType()),
entity.getReferenceId(),
Quantity.reconstitute(
entity.getQuantityAmount(),
UnitOfMeasure.valueOf(entity.getQuantityUnit())
),
ReservationPriority.valueOf(entity.getPriority()),
entity.getReservedAt(),
allocations
);
}
private StockBatchAllocation toDomainAllocation(StockBatchAllocationEntity entity) {
return new StockBatchAllocation(
AllocationId.of(entity.getId()),
StockBatchId.of(entity.getStockBatchId()),
Quantity.reconstitute(
entity.getAllocatedQuantityAmount(),
UnitOfMeasure.valueOf(entity.getAllocatedQuantityUnit())
)
);
}
}

View file

@ -1,51 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.mapper;
import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.infrastructure.inventory.persistence.entity.StockMovementEntity;
import org.springframework.stereotype.Component;
@Component
public class StockMovementMapper {
public StockMovementEntity toEntity(StockMovement movement) {
return new StockMovementEntity(
movement.id().value(),
movement.stockId().value(),
movement.articleId().value(),
movement.stockBatchId().value(),
movement.batchReference().batchId(),
movement.batchReference().batchType().name(),
movement.movementType().name(),
movement.direction().name(),
movement.quantity().amount(),
movement.quantity().uom().name(),
movement.reason(),
movement.referenceDocumentId(),
movement.performedBy(),
movement.performedAt()
);
}
public StockMovement toDomain(StockMovementEntity entity) {
return StockMovement.reconstitute(
StockMovementId.of(entity.getId()),
StockId.of(entity.getStockId()),
ArticleId.of(entity.getArticleId()),
StockBatchId.of(entity.getStockBatchId()),
new BatchReference(entity.getBatchId(), BatchType.valueOf(entity.getBatchType())),
MovementType.valueOf(entity.getMovementType()),
MovementDirection.valueOf(entity.getDirection()),
Quantity.reconstitute(
entity.getQuantityAmount(),
UnitOfMeasure.valueOf(entity.getQuantityUnit())
),
entity.getReason(),
entity.getReferenceDocumentId(),
entity.getPerformedBy(),
entity.getPerformedAt()
);
}
}

View file

@ -1,40 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.mapper;
import de.effigenix.domain.inventory.*;
import de.effigenix.infrastructure.inventory.persistence.entity.StorageLocationEntity;
import org.springframework.stereotype.Component;
@Component
public class StorageLocationMapper {
public StorageLocationEntity toEntity(StorageLocation location) {
var entity = new StorageLocationEntity();
entity.setId(location.id().value());
entity.setName(location.name().value());
entity.setStorageType(location.storageType().name());
entity.setActive(location.active());
var range = location.temperatureRange();
if (range != null) {
entity.setMinTemperature(range.minTemperature());
entity.setMaxTemperature(range.maxTemperature());
}
return entity;
}
public StorageLocation toDomain(StorageLocationEntity entity) {
TemperatureRange temperatureRange = null;
if (entity.getMinTemperature() != null && entity.getMaxTemperature() != null) {
temperatureRange = new TemperatureRange(entity.getMinTemperature(), entity.getMaxTemperature());
}
return StorageLocation.reconstitute(
StorageLocationId.of(entity.getId()),
new StorageLocationName(entity.getName()),
StorageType.valueOf(entity.getStorageType()),
temperatureRange,
entity.isActive()
);
}
}

View file

@ -0,0 +1,213 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@Repository
@Profile("!no-db")
public class JdbcStockMovementRepository implements StockMovementRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcStockMovementRepository.class);
private final JdbcClient jdbc;
public JdbcStockMovementRepository(JdbcClient jdbc) {
this.jdbc = jdbc;
}
@Override
public Result<RepositoryError, Optional<StockMovement>> findById(StockMovementId id) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE id = :id")
.param("id", id.value())
.query(this::mapRow)
.optional();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAll() {
try {
var result = jdbc.sql("SELECT * FROM stock_movements ORDER BY performed_at DESC")
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByStockId(StockId stockId) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE stock_id = :stockId ORDER BY performed_at DESC")
.param("stockId", stockId.value())
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByStockId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByArticleId(ArticleId articleId) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE article_id = :articleId ORDER BY performed_at DESC")
.param("articleId", articleId.value())
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByArticleId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByMovementType(MovementType movementType) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE movement_type = :movementType ORDER BY performed_at DESC")
.param("movementType", movementType.name())
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByMovementType", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByBatchReference(String batchReference) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE batch_id = :batchId ORDER BY performed_at DESC")
.param("batchId", batchReference)
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByBatchReference", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByPerformedAtBetween(Instant from, Instant to) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE performed_at >= :from AND performed_at <= :to ORDER BY performed_at DESC")
.param("from", from)
.param("to", to)
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByPerformedAtBetween", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByPerformedAtAfter(Instant from) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE performed_at >= :from ORDER BY performed_at DESC")
.param("from", from)
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByPerformedAtAfter", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByPerformedAtBefore(Instant to) {
try {
var result = jdbc.sql("SELECT * FROM stock_movements WHERE performed_at <= :to ORDER BY performed_at DESC")
.param("to", to)
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByPerformedAtBefore", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Void> save(StockMovement movement) {
try {
jdbc.sql("""
INSERT INTO stock_movements
(id, stock_id, article_id, stock_batch_id, batch_id, batch_type,
movement_type, direction, quantity_amount, quantity_unit,
reason, reference_document_id, performed_by, performed_at)
VALUES (:id, :stockId, :articleId, :stockBatchId, :batchId, :batchType,
:movementType, :direction, :quantityAmount, :quantityUnit,
:reason, :referenceDocumentId, :performedBy, :performedAt)
""")
.param("id", movement.id().value())
.param("stockId", movement.stockId().value())
.param("articleId", movement.articleId().value())
.param("stockBatchId", movement.stockBatchId().value())
.param("batchId", movement.batchReference().batchId())
.param("batchType", movement.batchReference().batchType().name())
.param("movementType", movement.movementType().name())
.param("direction", movement.direction().name())
.param("quantityAmount", movement.quantity().amount())
.param("quantityUnit", movement.quantity().uom().name())
.param("reason", movement.reason())
.param("referenceDocumentId", movement.referenceDocumentId())
.param("performedBy", movement.performedBy())
.param("performedAt", movement.performedAt())
.update();
return Result.success(null);
} catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
private StockMovement mapRow(ResultSet rs, int rowNum) throws SQLException {
return StockMovement.reconstitute(
StockMovementId.of(rs.getString("id")),
StockId.of(rs.getString("stock_id")),
ArticleId.of(rs.getString("article_id")),
StockBatchId.of(rs.getString("stock_batch_id")),
new BatchReference(rs.getString("batch_id"), BatchType.valueOf(rs.getString("batch_type"))),
MovementType.valueOf(rs.getString("movement_type")),
MovementDirection.valueOf(rs.getString("direction")),
Quantity.reconstitute(
rs.getBigDecimal("quantity_amount"),
UnitOfMeasure.valueOf(rs.getString("quantity_unit"))
),
rs.getString("reason"),
rs.getString("reference_document_id"),
rs.getString("performed_by"),
rs.getObject("performed_at", Instant.class)
);
}
}

View file

@ -0,0 +1,413 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
@Profile("!no-db")
public class JdbcStockRepository implements StockRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcStockRepository.class);
private final JdbcClient jdbc;
public JdbcStockRepository(JdbcClient jdbc) {
this.jdbc = jdbc;
}
@Override
public Result<RepositoryError, Optional<Stock>> findById(StockId id) {
try {
var stockOpt = jdbc.sql("SELECT * FROM stocks WHERE id = :id")
.param("id", id.value())
.query(this::mapStockRow)
.optional();
if (stockOpt.isEmpty()) {
return Result.success(Optional.empty());
}
return Result.success(Optional.of(loadChildren(stockOpt.get(), id.value())));
} catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
try {
var stockOpt = jdbc.sql("SELECT * FROM stocks WHERE article_id = :articleId AND storage_location_id = :storageLocationId")
.param("articleId", articleId.value())
.param("storageLocationId", storageLocationId.value())
.query(this::mapStockRow)
.optional();
if (stockOpt.isEmpty()) {
return Result.success(Optional.empty());
}
var stock = stockOpt.get();
return Result.success(Optional.of(loadChildren(stock, stock.id().value())));
} catch (Exception e) {
logger.trace("Database error in findByArticleIdAndStorageLocationId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
try {
int count = jdbc.sql("SELECT COUNT(*) FROM stocks WHERE article_id = :articleId AND storage_location_id = :storageLocationId")
.param("articleId", articleId.value())
.param("storageLocationId", storageLocationId.value())
.query(Integer.class)
.single();
return Result.success(count > 0);
} catch (Exception e) {
logger.trace("Database error in existsByArticleIdAndStorageLocationId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAll() {
try {
var stocks = jdbc.sql("SELECT * FROM stocks")
.query(this::mapStockRow)
.list();
return Result.success(loadChildrenForAll(stocks));
} catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId) {
try {
var stocks = jdbc.sql("SELECT * FROM stocks WHERE storage_location_id = :storageLocationId")
.param("storageLocationId", storageLocationId.value())
.query(this::mapStockRow)
.list();
return Result.success(loadChildrenForAll(stocks));
} catch (Exception e) {
logger.trace("Database error in findAllByStorageLocationId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllByArticleId(ArticleId articleId) {
try {
var stocks = jdbc.sql("SELECT * FROM stocks WHERE article_id = :articleId")
.param("articleId", articleId.value())
.query(this::mapStockRow)
.list();
return Result.success(loadChildrenForAll(stocks));
} catch (Exception e) {
logger.trace("Database error in findAllByArticleId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) {
try {
var stocks = jdbc.sql("""
SELECT DISTINCT s.* FROM stocks s
JOIN stock_batches b ON b.stock_id = s.id
WHERE (b.status IN ('AVAILABLE', 'EXPIRING_SOON') AND b.expiry_date < :today)
OR (s.minimum_shelf_life_days IS NOT NULL AND b.status = 'AVAILABLE'
AND b.expiry_date >= :today
AND b.expiry_date < :today + s.minimum_shelf_life_days * INTERVAL '1 day')
""")
.param("today", referenceDate)
.query(this::mapStockRow)
.list();
return Result.success(loadChildrenForAll(stocks));
} catch (Exception e) {
logger.trace("Database error in findAllWithExpiryRelevantBatches", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel() {
try {
var stocks = jdbc.sql("""
SELECT DISTINCT s.* FROM stocks s
LEFT JOIN stock_batches b ON b.stock_id = s.id AND b.status IN ('AVAILABLE', 'EXPIRING_SOON')
WHERE s.minimum_level_amount IS NOT NULL
GROUP BY s.id
HAVING COALESCE(SUM(b.quantity_amount), 0) < s.minimum_level_amount
""")
.query(this::mapStockRow)
.list();
return Result.success(loadChildrenForAll(stocks));
} catch (Exception e) {
logger.trace("Database error in findAllBelowMinimumLevel", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Void> save(Stock stock) {
try {
int rows = jdbc.sql("""
UPDATE stocks
SET article_id = :articleId, storage_location_id = :storageLocationId,
minimum_level_amount = :minimumLevelAmount, minimum_level_unit = :minimumLevelUnit,
minimum_shelf_life_days = :minimumShelfLifeDays
WHERE id = :id
""")
.param("id", stock.id().value())
.params(stockParams(stock))
.update();
if (rows == 0) {
jdbc.sql("""
INSERT INTO stocks (id, article_id, storage_location_id,
minimum_level_amount, minimum_level_unit, minimum_shelf_life_days)
VALUES (:id, :articleId, :storageLocationId,
:minimumLevelAmount, :minimumLevelUnit, :minimumShelfLifeDays)
""")
.param("id", stock.id().value())
.params(stockParams(stock))
.update();
}
saveChildren(stock);
return Result.success(null);
} catch (Exception e) {
logger.trace("Database error in save", e);
if (e.getMessage() != null && e.getMessage().contains("duplicate key")
|| e.getMessage() != null && e.getMessage().contains("unique constraint")) {
return Result.failure(new RepositoryError.DuplicateEntry(
"Stock already exists for article " + stock.articleId().value()
+ " at location " + stock.storageLocationId().value()));
}
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
// ==================== Private Helpers ====================
private java.util.Map<String, Object> stockParams(Stock stock) {
var params = new java.util.LinkedHashMap<String, Object>();
params.put("articleId", stock.articleId().value());
params.put("storageLocationId", stock.storageLocationId().value());
params.put("minimumLevelAmount", stock.minimumLevel() != null ? stock.minimumLevel().quantity().amount() : null);
params.put("minimumLevelUnit", stock.minimumLevel() != null ? stock.minimumLevel().quantity().uom().name() : null);
params.put("minimumShelfLifeDays", stock.minimumShelfLife() != null ? stock.minimumShelfLife().days() : null);
return params;
}
private void saveChildren(Stock stock) {
String stockId = stock.id().value();
// Delete reservations first (cascades to allocations via FK ON DELETE CASCADE)
// Must happen before deleting batches due to FK from allocations -> stock_batches
jdbc.sql("DELETE FROM reservations WHERE stock_id = :stockId")
.param("stockId", stockId)
.update();
// Delete + re-insert batches
jdbc.sql("DELETE FROM stock_batches WHERE stock_id = :stockId")
.param("stockId", stockId)
.update();
for (StockBatch batch : stock.batches()) {
jdbc.sql("""
INSERT INTO stock_batches
(id, stock_id, batch_id, batch_type, quantity_amount, quantity_unit, expiry_date, status, received_at)
VALUES (:id, :stockId, :batchId, :batchType, :quantityAmount, :quantityUnit, :expiryDate, :status, :receivedAt)
""")
.param("id", batch.id().value())
.param("stockId", stockId)
.param("batchId", batch.batchReference().batchId())
.param("batchType", batch.batchReference().batchType().name())
.param("quantityAmount", batch.quantity().amount())
.param("quantityUnit", batch.quantity().uom().name())
.param("expiryDate", batch.expiryDate())
.param("status", batch.status().name())
.param("receivedAt", batch.receivedAt())
.update();
}
for (Reservation reservation : stock.reservations()) {
jdbc.sql("""
INSERT INTO reservations
(id, stock_id, reference_type, reference_id, quantity_amount, quantity_unit, priority, reserved_at)
VALUES (:id, :stockId, :referenceType, :referenceId, :quantityAmount, :quantityUnit, :priority, :reservedAt)
""")
.param("id", reservation.id().value())
.param("stockId", stockId)
.param("referenceType", reservation.referenceType().name())
.param("referenceId", reservation.referenceId())
.param("quantityAmount", reservation.quantity().amount())
.param("quantityUnit", reservation.quantity().uom().name())
.param("priority", reservation.priority().name())
.param("reservedAt", reservation.reservedAt())
.update();
for (StockBatchAllocation allocation : reservation.allocations()) {
jdbc.sql("""
INSERT INTO stock_batch_allocations
(id, reservation_id, stock_batch_id, allocated_quantity_amount, allocated_quantity_unit)
VALUES (:id, :reservationId, :stockBatchId, :allocatedQuantityAmount, :allocatedQuantityUnit)
""")
.param("id", allocation.id().value())
.param("reservationId", reservation.id().value())
.param("stockBatchId", allocation.stockBatchId().value())
.param("allocatedQuantityAmount", allocation.allocatedQuantity().amount())
.param("allocatedQuantityUnit", allocation.allocatedQuantity().uom().name())
.update();
}
}
}
private Stock loadChildren(Stock stock, String stockId) {
var batches = jdbc.sql("SELECT * FROM stock_batches WHERE stock_id = :stockId")
.param("stockId", stockId)
.query(this::mapBatchRow)
.list();
var reservations = loadReservations(stockId);
return Stock.reconstitute(
stock.id(), stock.articleId(), stock.storageLocationId(),
stock.minimumLevel(), stock.minimumShelfLife(),
batches, reservations
);
}
private List<Stock> loadChildrenForAll(List<Stock> stocks) {
return stocks.stream()
.map(s -> loadChildren(s, s.id().value()))
.toList();
}
private List<Reservation> loadReservations(String stockId) {
var reservationRows = jdbc.sql("SELECT * FROM reservations WHERE stock_id = :stockId")
.param("stockId", stockId)
.query(this::mapReservationRow)
.list();
if (reservationRows.isEmpty()) {
return List.of();
}
List<String> reservationIds = reservationRows.stream()
.map(r -> r.id().value())
.toList();
var allAllocations = jdbc.sql("SELECT * FROM stock_batch_allocations WHERE reservation_id IN (:reservationIds)")
.param("reservationIds", reservationIds)
.query(this::mapAllocationRow)
.list();
// Group allocations by reservation_id
var allocationsByReservation = new java.util.HashMap<String, List<StockBatchAllocation>>();
for (var entry : allAllocations) {
allocationsByReservation.computeIfAbsent(entry.reservationId(), k -> new ArrayList<>())
.add(entry.allocation());
}
return reservationRows.stream()
.map(r -> new Reservation(
r.id(), r.referenceType(), r.referenceId(),
r.quantity(), r.priority(), r.reservedAt(),
allocationsByReservation.getOrDefault(r.id().value(), List.of())
))
.toList();
}
// ==================== Row Mappers ====================
private Stock mapStockRow(ResultSet rs, int rowNum) throws SQLException {
MinimumLevel minimumLevel = null;
var levelAmount = rs.getBigDecimal("minimum_level_amount");
String levelUnit = rs.getString("minimum_level_unit");
if (levelAmount != null && levelUnit != null) {
minimumLevel = new MinimumLevel(Quantity.reconstitute(levelAmount, UnitOfMeasure.valueOf(levelUnit)));
}
MinimumShelfLife minimumShelfLife = null;
int shelfLifeDays = rs.getInt("minimum_shelf_life_days");
if (!rs.wasNull()) {
minimumShelfLife = new MinimumShelfLife(shelfLifeDays);
}
return Stock.reconstitute(
StockId.of(rs.getString("id")),
ArticleId.of(rs.getString("article_id")),
StorageLocationId.of(rs.getString("storage_location_id")),
minimumLevel, minimumShelfLife,
List.of(), List.of()
);
}
private StockBatch mapBatchRow(ResultSet rs, int rowNum) throws SQLException {
return StockBatch.reconstitute(
StockBatchId.of(rs.getString("id")),
new BatchReference(rs.getString("batch_id"), BatchType.valueOf(rs.getString("batch_type"))),
Quantity.reconstitute(
rs.getBigDecimal("quantity_amount"),
UnitOfMeasure.valueOf(rs.getString("quantity_unit"))
),
rs.getObject("expiry_date", LocalDate.class),
StockBatchStatus.valueOf(rs.getString("status")),
rs.getObject("received_at", Instant.class)
);
}
private ReservationRow mapReservationRow(ResultSet rs, int rowNum) throws SQLException {
return new ReservationRow(
ReservationId.of(rs.getString("id")),
ReferenceType.valueOf(rs.getString("reference_type")),
rs.getString("reference_id"),
Quantity.reconstitute(
rs.getBigDecimal("quantity_amount"),
UnitOfMeasure.valueOf(rs.getString("quantity_unit"))
),
ReservationPriority.valueOf(rs.getString("priority")),
rs.getObject("reserved_at", Instant.class)
);
}
private AllocationRow mapAllocationRow(ResultSet rs, int rowNum) throws SQLException {
return new AllocationRow(
rs.getString("reservation_id"),
new StockBatchAllocation(
AllocationId.of(rs.getString("id")),
StockBatchId.of(rs.getString("stock_batch_id")),
Quantity.reconstitute(
rs.getBigDecimal("allocated_quantity_amount"),
UnitOfMeasure.valueOf(rs.getString("allocated_quantity_unit"))
)
)
);
}
private record ReservationRow(
ReservationId id, ReferenceType referenceType, String referenceId,
Quantity quantity, ReservationPriority priority, Instant reservedAt
) {}
private record AllocationRow(String reservationId, StockBatchAllocation allocation) {}
}

View file

@ -0,0 +1,170 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
@Repository
@Profile("!no-db")
public class JdbcStorageLocationRepository implements StorageLocationRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcStorageLocationRepository.class);
private final JdbcClient jdbc;
public JdbcStorageLocationRepository(JdbcClient jdbc) {
this.jdbc = jdbc;
}
@Override
public Result<RepositoryError, Optional<StorageLocation>> findById(StorageLocationId id) {
try {
var result = jdbc.sql("SELECT * FROM storage_locations WHERE id = :id")
.param("id", id.value())
.query(this::mapRow)
.optional();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StorageLocation>> findAll() {
try {
var result = jdbc.sql("SELECT * FROM storage_locations ORDER BY name")
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StorageLocation>> findByStorageType(StorageType storageType) {
try {
var result = jdbc.sql("SELECT * FROM storage_locations WHERE storage_type = :storageType ORDER BY name")
.param("storageType", storageType.name())
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findByStorageType", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StorageLocation>> findActive() {
try {
var result = jdbc.sql("SELECT * FROM storage_locations WHERE active = true ORDER BY name")
.query(this::mapRow)
.list();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findActive", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Boolean> existsByName(StorageLocationName name) {
try {
int count = jdbc.sql("SELECT COUNT(*) FROM storage_locations WHERE name = :name")
.param("name", name.value())
.query(Integer.class)
.single();
return Result.success(count > 0);
} catch (Exception e) {
logger.trace("Database error in existsByName", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Boolean> existsByNameAndIdNot(StorageLocationName name, StorageLocationId id) {
try {
int count = jdbc.sql("SELECT COUNT(*) FROM storage_locations WHERE name = :name AND id != :id")
.param("name", name.value())
.param("id", id.value())
.query(Integer.class)
.single();
return Result.success(count > 0);
} catch (Exception e) {
logger.trace("Database error in existsByNameAndIdNot", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Void> save(StorageLocation location) {
try {
int rows = jdbc.sql("""
UPDATE storage_locations
SET name = :name, storage_type = :storageType,
min_temperature = :minTemperature, max_temperature = :maxTemperature,
active = :active
WHERE id = :id
""")
.param("id", location.id().value())
.params(locationParams(location))
.update();
if (rows == 0) {
jdbc.sql("""
INSERT INTO storage_locations (id, name, storage_type, min_temperature, max_temperature, active)
VALUES (:id, :name, :storageType, :minTemperature, :maxTemperature, :active)
""")
.param("id", location.id().value())
.params(locationParams(location))
.update();
}
return Result.success(null);
} catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
private java.util.Map<String, Object> locationParams(StorageLocation location) {
var params = new java.util.LinkedHashMap<String, Object>();
params.put("name", location.name().value());
params.put("storageType", location.storageType().name());
params.put("minTemperature", location.temperatureRange() != null ? location.temperatureRange().minTemperature() : null);
params.put("maxTemperature", location.temperatureRange() != null ? location.temperatureRange().maxTemperature() : null);
params.put("active", location.active());
return params;
}
private StorageLocation mapRow(ResultSet rs, int rowNum) throws SQLException {
TemperatureRange temperatureRange = null;
BigDecimal minTemp = rs.getBigDecimal("min_temperature");
BigDecimal maxTemp = rs.getBigDecimal("max_temperature");
if (minTemp != null && maxTemp != null) {
temperatureRange = new TemperatureRange(minTemp, maxTemp);
}
return StorageLocation.reconstitute(
StorageLocationId.of(rs.getString("id")),
new StorageLocationName(rs.getString("name")),
StorageType.valueOf(rs.getString("storage_type")),
temperatureRange,
rs.getBoolean("active")
);
}
}

View file

@ -1,160 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.infrastructure.inventory.persistence.mapper.StockMovementMapper;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@Repository
@Profile("!no-db")
@Transactional(readOnly = true)
public class JpaStockMovementRepository implements StockMovementRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaStockMovementRepository.class);
private final StockMovementJpaRepository jpaRepository;
private final StockMovementMapper mapper;
public JpaStockMovementRepository(StockMovementJpaRepository jpaRepository, StockMovementMapper mapper) {
this.jpaRepository = jpaRepository;
this.mapper = mapper;
}
@Override
public Result<RepositoryError, Optional<StockMovement>> findById(StockMovementId id) {
try {
Optional<StockMovement> result = jpaRepository.findById(id.value())
.map(mapper::toDomain);
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAll() {
try {
List<StockMovement> result = jpaRepository.findAll().stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByStockId(StockId stockId) {
try {
List<StockMovement> result = jpaRepository.findAllByStockId(stockId.value()).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAllByStockId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByArticleId(ArticleId articleId) {
try {
List<StockMovement> result = jpaRepository.findAllByArticleId(articleId.value()).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAllByArticleId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByMovementType(MovementType movementType) {
try {
List<StockMovement> result = jpaRepository.findAllByMovementType(movementType.name()).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAllByMovementType", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByBatchReference(String batchReference) {
try {
List<StockMovement> result = jpaRepository.findAllByBatchId(batchReference).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAllByBatchReference", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByPerformedAtBetween(Instant from, Instant to) {
try {
List<StockMovement> result = jpaRepository.findAllByPerformedAtBetween(from, to).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAllByPerformedAtBetween", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByPerformedAtAfter(Instant from) {
try {
List<StockMovement> result = jpaRepository.findAllByPerformedAtGreaterThanEqual(from).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAllByPerformedAtAfter", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StockMovement>> findAllByPerformedAtBefore(Instant to) {
try {
List<StockMovement> result = jpaRepository.findAllByPerformedAtLessThanEqual(to).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.warn("Database error in findAllByPerformedAtBefore", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
@Transactional
public Result<RepositoryError, Void> save(StockMovement stockMovement) {
try {
jpaRepository.save(mapper.toEntity(stockMovement));
return Result.success(null);
} catch (Exception e) {
logger.warn("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
}

View file

@ -1,149 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.infrastructure.inventory.persistence.mapper.StockMapper;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
@Profile("!no-db")
@Transactional(readOnly = true)
public class JpaStockRepository implements StockRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaStockRepository.class);
private final StockJpaRepository jpaRepository;
private final StockMapper mapper;
public JpaStockRepository(StockJpaRepository jpaRepository, StockMapper mapper) {
this.jpaRepository = jpaRepository;
this.mapper = mapper;
}
@Override
public Result<RepositoryError, Optional<Stock>> findById(StockId id) {
try {
Optional<Stock> result = jpaRepository.findById(id.value())
.map(mapper::toDomain);
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
try {
Optional<Stock> result = jpaRepository.findByArticleIdAndStorageLocationId(articleId.value(), storageLocationId.value())
.map(mapper::toDomain);
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findByArticleIdAndStorageLocationId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
try {
return Result.success(jpaRepository.existsByArticleIdAndStorageLocationId(articleId.value(), storageLocationId.value()));
} catch (Exception e) {
logger.trace("Database error in existsByArticleIdAndStorageLocationId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAll() {
try {
List<Stock> result = jpaRepository.findAll().stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId) {
try {
List<Stock> result = jpaRepository.findAllByStorageLocationId(storageLocationId.value()).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByStorageLocationId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllByArticleId(ArticleId articleId) {
try {
List<Stock> result = jpaRepository.findAllByArticleId(articleId.value()).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllByArticleId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) {
try {
List<Stock> result = jpaRepository.findAllWithExpiryRelevantBatches(referenceDate).stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllWithExpiryRelevantBatches", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel() {
try {
List<Stock> result = jpaRepository.findAllBelowMinimumLevel().stream()
.map(mapper::toDomain)
.toList();
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllBelowMinimumLevel", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
@Transactional
public Result<RepositoryError, Void> save(Stock stock) {
try {
jpaRepository.save(mapper.toEntity(stock));
return Result.success(null);
} catch (DataIntegrityViolationException e) {
logger.trace("Duplicate entry in save", e);
return Result.failure(new RepositoryError.DuplicateEntry(
"Stock already exists for article " + stock.articleId().value()
+ " at location " + stock.storageLocationId().value()));
} catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
}

View file

@ -1,114 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*;
import de.effigenix.infrastructure.inventory.persistence.mapper.StorageLocationMapper;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Repository
@Profile("!no-db")
@Transactional(readOnly = true)
public class JpaStorageLocationRepository implements StorageLocationRepository {
private static final Logger logger = LoggerFactory.getLogger(JpaStorageLocationRepository.class);
private final StorageLocationJpaRepository jpaRepository;
private final StorageLocationMapper mapper;
public JpaStorageLocationRepository(StorageLocationJpaRepository jpaRepository, StorageLocationMapper mapper) {
this.jpaRepository = jpaRepository;
this.mapper = mapper;
}
@Override
public Result<RepositoryError, Optional<StorageLocation>> findById(StorageLocationId id) {
try {
Optional<StorageLocation> result = jpaRepository.findById(id.value())
.map(mapper::toDomain);
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findById", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StorageLocation>> findAll() {
try {
List<StorageLocation> result = jpaRepository.findAll().stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StorageLocation>> findByStorageType(StorageType storageType) {
try {
List<StorageLocation> result = jpaRepository.findByStorageType(storageType.name()).stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findByStorageType", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<StorageLocation>> findActive() {
try {
List<StorageLocation> result = jpaRepository.findByActiveTrue().stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findActive", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Boolean> existsByName(StorageLocationName name) {
try {
return Result.success(jpaRepository.existsByName(name.value()));
} catch (Exception e) {
logger.trace("Database error in existsByName", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@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) {
try {
jpaRepository.save(mapper.toEntity(location));
return Result.success(null);
} catch (Exception e) {
logger.trace("Database error in save", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
}

View file

@ -1,40 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.infrastructure.inventory.persistence.entity.StockEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface StockJpaRepository extends JpaRepository<StockEntity, String> {
Optional<StockEntity> findByArticleIdAndStorageLocationId(String articleId, String storageLocationId);
boolean existsByArticleIdAndStorageLocationId(String articleId, String storageLocationId);
List<StockEntity> findAllByStorageLocationId(String storageLocationId);
List<StockEntity> findAllByArticleId(String articleId);
@Query(value = """
SELECT DISTINCT s.* FROM stocks s \
JOIN stock_batches b ON b.stock_id = s.id \
WHERE (b.status IN ('AVAILABLE', 'EXPIRING_SOON') AND b.expiry_date < :today) \
OR (s.minimum_shelf_life_days IS NOT NULL AND b.status = 'AVAILABLE' \
AND b.expiry_date >= :today \
AND b.expiry_date < :today + s.minimum_shelf_life_days * INTERVAL '1 day')""",
nativeQuery = true)
List<StockEntity> findAllWithExpiryRelevantBatches(@Param("today") LocalDate today);
@Query(value = """
SELECT DISTINCT s.* FROM stocks s \
LEFT JOIN stock_batches b ON b.stock_id = s.id AND b.status IN ('AVAILABLE', 'EXPIRING_SOON') \
WHERE s.minimum_level_amount IS NOT NULL \
GROUP BY s.id \
HAVING COALESCE(SUM(b.quantity_amount), 0) < s.minimum_level_amount""",
nativeQuery = true)
List<StockEntity> findAllBelowMinimumLevel();
}

View file

@ -1,24 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.infrastructure.inventory.persistence.entity.StockMovementEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.Instant;
import java.util.List;
public interface StockMovementJpaRepository extends JpaRepository<StockMovementEntity, String> {
List<StockMovementEntity> findAllByStockId(String stockId);
List<StockMovementEntity> findAllByArticleId(String articleId);
List<StockMovementEntity> findAllByMovementType(String movementType);
List<StockMovementEntity> findAllByBatchId(String batchId);
List<StockMovementEntity> findAllByPerformedAtBetween(Instant from, Instant to);
List<StockMovementEntity> findAllByPerformedAtGreaterThanEqual(Instant from);
List<StockMovementEntity> findAllByPerformedAtLessThanEqual(Instant to);
}

View file

@ -1,17 +0,0 @@
package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.infrastructure.inventory.persistence.entity.StorageLocationEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface StorageLocationJpaRepository extends JpaRepository<StorageLocationEntity, String> {
List<StorageLocationEntity> findByStorageType(String storageType);
List<StorageLocationEntity> findByActiveTrue();
boolean existsByName(String name);
boolean existsByNameAndIdNot(String name, String id);
}

View file

@ -6,6 +6,7 @@ import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.persistence.UnitOfWork;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -28,6 +29,7 @@ import static org.mockito.Mockito.*;
class AddStockBatchTest { class AddStockBatchTest {
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private UnitOfWork unitOfWork;
private AddStockBatch addStockBatch; private AddStockBatch addStockBatch;
private AddStockBatchCommand validCommand; private AddStockBatchCommand validCommand;
@ -35,7 +37,8 @@ class AddStockBatchTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
addStockBatch = new AddStockBatch(stockRepository); addStockBatch = new AddStockBatch(stockRepository, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
existingStock = Stock.reconstitute( existingStock = Stock.reconstitute(
StockId.of("stock-1"), StockId.of("stock-1"),

View file

@ -7,6 +7,7 @@ import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@ -32,6 +33,7 @@ class BlockStockBatchTest {
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private AuditLogger auditLogger; @Mock private AuditLogger auditLogger;
@Mock private UnitOfWork unitOfWork;
private BlockStockBatch blockStockBatch; private BlockStockBatch blockStockBatch;
private StockBatchId batchId; private StockBatchId batchId;
@ -41,7 +43,8 @@ class BlockStockBatchTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
blockStockBatch = new BlockStockBatch(stockRepository, auditLogger); blockStockBatch = new BlockStockBatch(stockRepository, auditLogger, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
batchId = StockBatchId.of("batch-1"); batchId = StockBatchId.of("batch-1");
var batch = StockBatch.reconstitute( var batch = StockBatch.reconstitute(

View file

@ -4,6 +4,7 @@ import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId; import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@ -17,9 +18,8 @@ import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@DisplayName("DeactivateStorageLocation Use Case") @DisplayName("DeactivateStorageLocation Use Case")
@ -27,13 +27,15 @@ class DeactivateStorageLocationTest {
@Mock private StorageLocationRepository storageLocationRepository; @Mock private StorageLocationRepository storageLocationRepository;
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private UnitOfWork unitOfWork;
private DeactivateStorageLocation deactivateStorageLocation; private DeactivateStorageLocation deactivateStorageLocation;
private ActorId performedBy; private ActorId performedBy;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
deactivateStorageLocation = new DeactivateStorageLocation(storageLocationRepository, stockRepository); deactivateStorageLocation = new DeactivateStorageLocation(storageLocationRepository, stockRepository, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
performedBy = ActorId.of("admin-user"); performedBy = ActorId.of("admin-user");
} }

View file

@ -4,6 +4,7 @@ import de.effigenix.application.inventory.command.RecordStockMovementCommand;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -19,6 +20,7 @@ class RecordStockMovementTest {
private StockMovementRepository repository; private StockMovementRepository repository;
private AuthorizationPort authPort; private AuthorizationPort authPort;
private UnitOfWork unitOfWork;
private RecordStockMovement useCase; private RecordStockMovement useCase;
private final ActorId actor = ActorId.of("user-1"); private final ActorId actor = ActorId.of("user-1");
@ -26,7 +28,9 @@ class RecordStockMovementTest {
void setUp() { void setUp() {
repository = mock(StockMovementRepository.class); repository = mock(StockMovementRepository.class);
authPort = mock(AuthorizationPort.class); authPort = mock(AuthorizationPort.class);
useCase = new RecordStockMovement(repository, authPort); unitOfWork = mock(UnitOfWork.class);
useCase = new RecordStockMovement(repository, authPort, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
when(authPort.can(any(ActorId.class), any())).thenReturn(true); when(authPort.can(any(ActorId.class), any())).thenReturn(true);
} }

View file

@ -6,6 +6,7 @@ import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.persistence.UnitOfWork;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -29,6 +30,7 @@ import static org.mockito.Mockito.*;
class ReleaseReservationTest { class ReleaseReservationTest {
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private UnitOfWork unitOfWork;
private ReleaseReservation releaseReservation; private ReleaseReservation releaseReservation;
private Stock existingStock; private Stock existingStock;
@ -36,7 +38,8 @@ class ReleaseReservationTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
releaseReservation = new ReleaseReservation(stockRepository); releaseReservation = new ReleaseReservation(stockRepository, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
var batch = StockBatch.reconstitute( var batch = StockBatch.reconstitute(
StockBatchId.generate(), StockBatchId.generate(),

View file

@ -6,6 +6,7 @@ import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.persistence.UnitOfWork;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -29,6 +30,7 @@ import static org.mockito.Mockito.*;
class RemoveStockBatchTest { class RemoveStockBatchTest {
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private UnitOfWork unitOfWork;
private RemoveStockBatch removeStockBatch; private RemoveStockBatch removeStockBatch;
private StockBatchId batchId; private StockBatchId batchId;
@ -37,7 +39,8 @@ class RemoveStockBatchTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
removeStockBatch = new RemoveStockBatch(stockRepository); removeStockBatch = new RemoveStockBatch(stockRepository, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
batchId = StockBatchId.of("batch-1"); batchId = StockBatchId.of("batch-1");
var batch = StockBatch.reconstitute( var batch = StockBatch.reconstitute(

View file

@ -6,6 +6,7 @@ import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.persistence.UnitOfWork;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -29,6 +30,7 @@ import static org.mockito.Mockito.*;
class ReserveStockTest { class ReserveStockTest {
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private UnitOfWork unitOfWork;
private ReserveStock reserveStock; private ReserveStock reserveStock;
private ReserveStockCommand validCommand; private ReserveStockCommand validCommand;
@ -36,7 +38,8 @@ class ReserveStockTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
reserveStock = new ReserveStock(stockRepository); reserveStock = new ReserveStock(stockRepository, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
var batch = StockBatch.reconstitute( var batch = StockBatch.reconstitute(
StockBatchId.generate(), StockBatchId.generate(),

View file

@ -7,6 +7,7 @@ import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@ -32,6 +33,7 @@ class UnblockStockBatchTest {
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private AuditLogger auditLogger; @Mock private AuditLogger auditLogger;
@Mock private UnitOfWork unitOfWork;
private UnblockStockBatch unblockStockBatch; private UnblockStockBatch unblockStockBatch;
private StockBatchId batchId; private StockBatchId batchId;
@ -41,7 +43,8 @@ class UnblockStockBatchTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
unblockStockBatch = new UnblockStockBatch(stockRepository, auditLogger); unblockStockBatch = new UnblockStockBatch(stockRepository, auditLogger, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
batchId = StockBatchId.of("batch-1"); batchId = StockBatchId.of("batch-1");
var batch = StockBatch.reconstitute( var batch = StockBatch.reconstitute(

View file

@ -5,6 +5,7 @@ import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.shared.persistence.UnitOfWork;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -18,19 +19,22 @@ import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@DisplayName("UpdateStock Use Case") @DisplayName("UpdateStock Use Case")
class UpdateStockTest { class UpdateStockTest {
@Mock private StockRepository stockRepository; @Mock private StockRepository stockRepository;
@Mock private UnitOfWork unitOfWork;
private UpdateStock updateStock; private UpdateStock updateStock;
private Stock existingStock; private Stock existingStock;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
updateStock = new UpdateStock(stockRepository); updateStock = new UpdateStock(stockRepository, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((java.util.function.Supplier<?>) inv.getArgument(0)).get());
existingStock = Stock.reconstitute( existingStock = Stock.reconstitute(
StockId.of("stock-1"), StockId.of("stock-1"),