From d4ac8cb1b95ea68cae169a190245f7b29c81bbf2 Mon Sep 17 00:00:00 2001 From: Sebastian Frick Date: Wed, 25 Feb 2026 12:57:46 +0100 Subject: [PATCH] refactor(inventory): UnitOfWork-Pattern + JdbcClient-Migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../inventory/ActivateStorageLocation.java | 22 +- .../application/inventory/AddStockBatch.java | 22 +- .../inventory/BlockStockBatch.java | 24 +- .../application/inventory/CreateStock.java | 28 +- .../inventory/CreateStorageLocation.java | 22 +- .../inventory/DeactivateStorageLocation.java | 22 +- .../application/inventory/GetStock.java | 2 - .../inventory/GetStockMovement.java | 2 - .../inventory/GetStorageLocation.java | 2 - .../inventory/ListStockMovements.java | 2 - .../application/inventory/ListStocks.java | 2 - .../inventory/ListStocksBelowMinimum.java | 2 - .../inventory/ListStorageLocations.java | 2 - .../inventory/RecordStockMovement.java | 22 +- .../inventory/ReleaseReservation.java | 22 +- .../inventory/RemoveStockBatch.java | 22 +- .../application/inventory/ReserveStock.java | 22 +- .../inventory/UnblockStockBatch.java | 24 +- .../application/inventory/UpdateStock.java | 22 +- .../inventory/UpdateStorageLocation.java | 22 +- .../config/InventoryUseCaseConfiguration.java | 53 +-- .../persistence/entity/ReservationEntity.java | 76 ---- .../entity/StockBatchAllocationEntity.java | 49 --- .../persistence/entity/StockBatchEntity.java | 77 ---- .../persistence/entity/StockEntity.java | 65 --- .../entity/StockMovementEntity.java | 120 ----- .../entity/StorageLocationEntity.java | 51 --- .../persistence/mapper/StockMapper.java | 168 ------- .../mapper/StockMovementMapper.java | 51 --- .../mapper/StorageLocationMapper.java | 40 -- .../JdbcStockMovementRepository.java | 213 +++++++++ .../repository/JdbcStockRepository.java | 413 ++++++++++++++++++ .../JdbcStorageLocationRepository.java | 170 +++++++ .../JpaStockMovementRepository.java | 160 ------- .../repository/JpaStockRepository.java | 149 ------- .../JpaStorageLocationRepository.java | 114 ----- .../repository/StockJpaRepository.java | 40 -- .../StockMovementJpaRepository.java | 24 - .../StorageLocationJpaRepository.java | 17 - .../inventory/AddStockBatchTest.java | 5 +- .../inventory/BlockStockBatchTest.java | 5 +- .../DeactivateStorageLocationTest.java | 10 +- .../inventory/RecordStockMovementTest.java | 6 +- .../inventory/ReleaseReservationTest.java | 5 +- .../inventory/RemoveStockBatchTest.java | 5 +- .../inventory/ReserveStockTest.java | 5 +- .../inventory/UnblockStockBatchTest.java | 5 +- .../inventory/UpdateStockTest.java | 6 +- 48 files changed, 1024 insertions(+), 1388 deletions(-) delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/ReservationEntity.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchAllocationEntity.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchEntity.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockEntity.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockMovementEntity.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StorageLocationEntity.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMapper.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMovementMapper.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StorageLocationMapper.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockMovementRepository.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockRepository.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockJpaRepository.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockMovementJpaRepository.java delete mode 100644 backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java diff --git a/backend/src/main/java/de/effigenix/application/inventory/ActivateStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/ActivateStorageLocation.java index 7dc99ad..2231983 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ActivateStorageLocation.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ActivateStorageLocation.java @@ -2,16 +2,17 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; -import org.springframework.transaction.annotation.Transactional; -@Transactional public class ActivateStorageLocation { private final StorageLocationRepository storageLocationRepository; + private final UnitOfWork unitOfWork; - public ActivateStorageLocation(StorageLocationRepository storageLocationRepository) { + public ActivateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) { this.storageLocationRepository = storageLocationRepository; + this.unitOfWork = unitOfWork; } public Result execute(String storageLocationId, ActorId performedBy) { @@ -36,12 +37,13 @@ public class ActivateStorageLocation { } // 3. Speichern - switch (storageLocationRepository.save(location)) { - case Result.Failure(var err) -> - { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(location); + return unitOfWork.executeAtomically(() -> { + switch (storageLocationRepository.save(location)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(location); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/AddStockBatch.java b/backend/src/main/java/de/effigenix/application/inventory/AddStockBatch.java index b1d1cbf..58ccf0c 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/AddStockBatch.java +++ b/backend/src/main/java/de/effigenix/application/inventory/AddStockBatch.java @@ -3,15 +3,16 @@ package de.effigenix.application.inventory; import de.effigenix.application.inventory.command.AddStockBatchCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; +import de.effigenix.shared.persistence.UnitOfWork; -@Transactional public class AddStockBatch { private final StockRepository stockRepository; + private final UnitOfWork unitOfWork; - public AddStockBatch(StockRepository stockRepository) { + public AddStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) { this.stockRepository = stockRepository; + this.unitOfWork = unitOfWork; } public Result execute(AddStockBatchCommand cmd) { @@ -42,12 +43,13 @@ public class AddStockBatch { } // 3. Stock speichern - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> - { return Result.failure(new StockError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(batch); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> + { return Result.failure(new StockError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(batch); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/BlockStockBatch.java b/backend/src/main/java/de/effigenix/application/inventory/BlockStockBatch.java index 048b4e8..9c6857e 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/BlockStockBatch.java +++ b/backend/src/main/java/de/effigenix/application/inventory/BlockStockBatch.java @@ -5,18 +5,19 @@ import de.effigenix.application.usermanagement.AuditEvent; import de.effigenix.application.usermanagement.AuditLogger; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; -import org.springframework.transaction.annotation.Transactional; -@Transactional public class BlockStockBatch { private final StockRepository stockRepository; 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.auditLogger = auditLogger; + this.unitOfWork = unitOfWork; } public Result execute(BlockStockBatchCommand cmd, ActorId performedBy) { @@ -40,13 +41,14 @@ public class BlockStockBatch { } // 3. Stock speichern - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> - { 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); - return Result.success(null); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> + { 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); + return Result.success(null); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/CreateStock.java b/backend/src/main/java/de/effigenix/application/inventory/CreateStock.java index e46bc96..7e9ff47 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/CreateStock.java +++ b/backend/src/main/java/de/effigenix/application/inventory/CreateStock.java @@ -4,15 +4,16 @@ import de.effigenix.application.inventory.command.CreateStockCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; +import de.effigenix.shared.persistence.UnitOfWork; -@Transactional public class CreateStock { private final StockRepository stockRepository; + private final UnitOfWork unitOfWork; - public CreateStock(StockRepository stockRepository) { + public CreateStock(StockRepository stockRepository, UnitOfWork unitOfWork) { this.stockRepository = stockRepository; + this.unitOfWork = unitOfWork; } public Result execute(CreateStockCommand cmd) { @@ -43,17 +44,18 @@ public class CreateStock { } // 4. Speichern (Race-Condition-Schutz: DuplicateEntry → DuplicateStock) - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> { - if (err instanceof RepositoryError.DuplicateEntry) { - return Result.failure(new StockError.DuplicateStock( - cmd.articleId(), cmd.storageLocationId())); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> { + if (err instanceof RepositoryError.DuplicateEntry) { + 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); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/CreateStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/CreateStorageLocation.java index 842a47b..1abc647 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/CreateStorageLocation.java +++ b/backend/src/main/java/de/effigenix/application/inventory/CreateStorageLocation.java @@ -3,16 +3,17 @@ package de.effigenix.application.inventory; import de.effigenix.application.inventory.command.CreateStorageLocationCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; -import org.springframework.transaction.annotation.Transactional; -@Transactional public class CreateStorageLocation { private final StorageLocationRepository storageLocationRepository; + private final UnitOfWork unitOfWork; - public CreateStorageLocation(StorageLocationRepository storageLocationRepository) { + public CreateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) { this.storageLocationRepository = storageLocationRepository; + this.unitOfWork = unitOfWork; } public Result execute(CreateStorageLocationCommand cmd, ActorId performedBy) { @@ -40,12 +41,13 @@ public class CreateStorageLocation { } // 4. Speichern - switch (storageLocationRepository.save(location)) { - case Result.Failure(var err) -> - { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(location); + return unitOfWork.executeAtomically(() -> { + switch (storageLocationRepository.save(location)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(location); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/DeactivateStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/DeactivateStorageLocation.java index 083abfb..ac7927c 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/DeactivateStorageLocation.java +++ b/backend/src/main/java/de/effigenix/application/inventory/DeactivateStorageLocation.java @@ -2,18 +2,19 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; -import org.springframework.transaction.annotation.Transactional; -@Transactional public class DeactivateStorageLocation { private final StorageLocationRepository storageLocationRepository; 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.stockRepository = stockRepository; + this.unitOfWork = unitOfWork; } public Result execute(String storageLocationId, ActorId performedBy) { @@ -49,12 +50,13 @@ public class DeactivateStorageLocation { } // 4. Speichern - switch (storageLocationRepository.save(location)) { - case Result.Failure(var err) -> - { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(location); + return unitOfWork.executeAtomically(() -> { + switch (storageLocationRepository.save(location)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(location); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/GetStock.java b/backend/src/main/java/de/effigenix/application/inventory/GetStock.java index d277f7e..601fa76 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/GetStock.java +++ b/backend/src/main/java/de/effigenix/application/inventory/GetStock.java @@ -5,9 +5,7 @@ import de.effigenix.domain.inventory.StockError; import de.effigenix.domain.inventory.StockId; import de.effigenix.domain.inventory.StockRepository; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; -@Transactional(readOnly = true) public class GetStock { private final StockRepository stockRepository; diff --git a/backend/src/main/java/de/effigenix/application/inventory/GetStockMovement.java b/backend/src/main/java/de/effigenix/application/inventory/GetStockMovement.java index 123df50..7f983c1 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/GetStockMovement.java +++ b/backend/src/main/java/de/effigenix/application/inventory/GetStockMovement.java @@ -8,9 +8,7 @@ import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; -import org.springframework.transaction.annotation.Transactional; -@Transactional(readOnly = true) public class GetStockMovement { private final StockMovementRepository stockMovementRepository; diff --git a/backend/src/main/java/de/effigenix/application/inventory/GetStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/GetStorageLocation.java index ca77d64..661c32d 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/GetStorageLocation.java +++ b/backend/src/main/java/de/effigenix/application/inventory/GetStorageLocation.java @@ -8,9 +8,7 @@ import de.effigenix.domain.inventory.StorageLocationRepository; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; -import org.springframework.transaction.annotation.Transactional; -@Transactional(readOnly = true) public class GetStorageLocation { private final StorageLocationRepository storageLocationRepository; diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java b/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java index acf5d88..5efc8bb 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListStockMovements.java @@ -11,12 +11,10 @@ import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; -import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.List; -@Transactional(readOnly = true) public class ListStockMovements { private final StockMovementRepository stockMovementRepository; diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java b/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java index 8dbd9a5..324d8c1 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListStocks.java @@ -7,11 +7,9 @@ import de.effigenix.domain.inventory.StorageLocationId; import de.effigenix.domain.masterdata.ArticleId; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; import java.util.List; -@Transactional(readOnly = true) public class ListStocks { private final StockRepository stockRepository; diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListStocksBelowMinimum.java b/backend/src/main/java/de/effigenix/application/inventory/ListStocksBelowMinimum.java index 511fded..653effb 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListStocksBelowMinimum.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListStocksBelowMinimum.java @@ -7,7 +7,6 @@ import de.effigenix.domain.inventory.StockRepository; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; -import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -21,7 +20,6 @@ public class ListStocksBelowMinimum { this.authPort = authPort; } - @Transactional(readOnly = true) public Result> execute(ActorId performedBy) { if (!authPort.can(performedBy, InventoryAction.STOCK_READ)) { return Result.failure(new StockError.Unauthorized("Not authorized to list stocks below minimum")); diff --git a/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java b/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java index ed6507e..d7a1d34 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ListStorageLocations.java @@ -3,11 +3,9 @@ package de.effigenix.application.inventory; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; import java.util.List; -@Transactional(readOnly = true) public class ListStorageLocations { private final StorageLocationRepository storageLocationRepository; diff --git a/backend/src/main/java/de/effigenix/application/inventory/RecordStockMovement.java b/backend/src/main/java/de/effigenix/application/inventory/RecordStockMovement.java index cee956c..ed2f9eb 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/RecordStockMovement.java +++ b/backend/src/main/java/de/effigenix/application/inventory/RecordStockMovement.java @@ -7,19 +7,20 @@ import de.effigenix.domain.inventory.StockMovementDraft; import de.effigenix.domain.inventory.StockMovementError; import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; -import org.springframework.transaction.annotation.Transactional; -@Transactional public class RecordStockMovement { private final StockMovementRepository stockMovementRepository; 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.authPort = authPort; + this.unitOfWork = unitOfWork; } public Result execute(RecordStockMovementCommand cmd, ActorId performedBy) { @@ -42,12 +43,13 @@ public class RecordStockMovement { case Result.Success(var val) -> movement = val; } - switch (stockMovementRepository.save(movement)) { - case Result.Failure(var err) -> - { return Result.failure(new StockMovementError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(movement); + return unitOfWork.executeAtomically(() -> { + switch (stockMovementRepository.save(movement)) { + case Result.Failure(var err) -> + { return Result.failure(new StockMovementError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(movement); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/ReleaseReservation.java b/backend/src/main/java/de/effigenix/application/inventory/ReleaseReservation.java index c0a4b95..840ed64 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ReleaseReservation.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ReleaseReservation.java @@ -3,15 +3,16 @@ package de.effigenix.application.inventory; import de.effigenix.application.inventory.command.ReleaseReservationCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; +import de.effigenix.shared.persistence.UnitOfWork; -@Transactional public class ReleaseReservation { private final StockRepository stockRepository; + private final UnitOfWork unitOfWork; - public ReleaseReservation(StockRepository stockRepository) { + public ReleaseReservation(StockRepository stockRepository, UnitOfWork unitOfWork) { this.stockRepository = stockRepository; + this.unitOfWork = unitOfWork; } public Result execute(ReleaseReservationCommand cmd) { @@ -35,12 +36,13 @@ public class ReleaseReservation { } // 3. Speichern - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> - { return Result.failure(new StockError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(null); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> + { return Result.failure(new StockError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(null); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/RemoveStockBatch.java b/backend/src/main/java/de/effigenix/application/inventory/RemoveStockBatch.java index a1e8d4d..dfdfd04 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/RemoveStockBatch.java +++ b/backend/src/main/java/de/effigenix/application/inventory/RemoveStockBatch.java @@ -5,17 +5,18 @@ import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Result; import de.effigenix.shared.common.UnitOfMeasure; -import org.springframework.transaction.annotation.Transactional; +import de.effigenix.shared.persistence.UnitOfWork; import java.math.BigDecimal; -@Transactional public class RemoveStockBatch { private final StockRepository stockRepository; + private final UnitOfWork unitOfWork; - public RemoveStockBatch(StockRepository stockRepository) { + public RemoveStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) { this.stockRepository = stockRepository; + this.unitOfWork = unitOfWork; } public Result execute(RemoveStockBatchCommand cmd) { @@ -59,12 +60,13 @@ public class RemoveStockBatch { } // 4. Stock speichern - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> - { return Result.failure(new StockError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(null); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> + { return Result.failure(new StockError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(null); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/ReserveStock.java b/backend/src/main/java/de/effigenix/application/inventory/ReserveStock.java index c46fac6..d8eaad1 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/ReserveStock.java +++ b/backend/src/main/java/de/effigenix/application/inventory/ReserveStock.java @@ -3,15 +3,16 @@ package de.effigenix.application.inventory; import de.effigenix.application.inventory.command.ReserveStockCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; +import de.effigenix.shared.persistence.UnitOfWork; -@Transactional public class ReserveStock { private final StockRepository stockRepository; + private final UnitOfWork unitOfWork; - public ReserveStock(StockRepository stockRepository) { + public ReserveStock(StockRepository stockRepository, UnitOfWork unitOfWork) { this.stockRepository = stockRepository; + this.unitOfWork = unitOfWork; } public Result execute(ReserveStockCommand cmd) { @@ -47,12 +48,13 @@ public class ReserveStock { } // 4. Speichern - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> - { return Result.failure(new StockError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(reservation); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> + { return Result.failure(new StockError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(reservation); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/UnblockStockBatch.java b/backend/src/main/java/de/effigenix/application/inventory/UnblockStockBatch.java index ca21978..e80939c 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/UnblockStockBatch.java +++ b/backend/src/main/java/de/effigenix/application/inventory/UnblockStockBatch.java @@ -5,20 +5,21 @@ import de.effigenix.application.usermanagement.AuditEvent; import de.effigenix.application.usermanagement.AuditLogger; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; -import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; -@Transactional public class UnblockStockBatch { private final StockRepository stockRepository; private final AuditLogger auditLogger; + private final UnitOfWork unitOfWork; - public UnblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) { + public UnblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger, UnitOfWork unitOfWork) { this.stockRepository = stockRepository; this.auditLogger = auditLogger; + this.unitOfWork = unitOfWork; } public Result execute(UnblockStockBatchCommand cmd, ActorId performedBy) { @@ -42,13 +43,14 @@ public class UnblockStockBatch { } // 3. Stock speichern - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> - { return Result.failure(new StockError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - auditLogger.log(AuditEvent.STOCK_BATCH_UNBLOCKED, cmd.batchId(), performedBy); - return Result.success(null); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> + { return Result.failure(new StockError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + auditLogger.log(AuditEvent.STOCK_BATCH_UNBLOCKED, cmd.batchId(), performedBy); + return Result.success(null); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/UpdateStock.java b/backend/src/main/java/de/effigenix/application/inventory/UpdateStock.java index b0c2005..3661ead 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/UpdateStock.java +++ b/backend/src/main/java/de/effigenix/application/inventory/UpdateStock.java @@ -3,15 +3,16 @@ package de.effigenix.application.inventory; import de.effigenix.application.inventory.command.UpdateStockCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; -import org.springframework.transaction.annotation.Transactional; +import de.effigenix.shared.persistence.UnitOfWork; -@Transactional public class UpdateStock { private final StockRepository stockRepository; + private final UnitOfWork unitOfWork; - public UpdateStock(StockRepository stockRepository) { + public UpdateStock(StockRepository stockRepository, UnitOfWork unitOfWork) { this.stockRepository = stockRepository; + this.unitOfWork = unitOfWork; } public Result execute(UpdateStockCommand cmd) { @@ -46,12 +47,13 @@ public class UpdateStock { } // 3. Speichern - switch (stockRepository.save(stock)) { - case Result.Failure(var err) -> - { return Result.failure(new StockError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(stock); + return unitOfWork.executeAtomically(() -> { + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> + { return Result.failure(new StockError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(stock); + }); } } diff --git a/backend/src/main/java/de/effigenix/application/inventory/UpdateStorageLocation.java b/backend/src/main/java/de/effigenix/application/inventory/UpdateStorageLocation.java index 3f49052..e8cb134 100644 --- a/backend/src/main/java/de/effigenix/application/inventory/UpdateStorageLocation.java +++ b/backend/src/main/java/de/effigenix/application/inventory/UpdateStorageLocation.java @@ -3,16 +3,17 @@ package de.effigenix.application.inventory; import de.effigenix.application.inventory.command.UpdateStorageLocationCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; -import org.springframework.transaction.annotation.Transactional; -@Transactional public class UpdateStorageLocation { private final StorageLocationRepository storageLocationRepository; + private final UnitOfWork unitOfWork; - public UpdateStorageLocation(StorageLocationRepository storageLocationRepository) { + public UpdateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) { this.storageLocationRepository = storageLocationRepository; + this.unitOfWork = unitOfWork; } public Result execute(UpdateStorageLocationCommand cmd, ActorId performedBy) { @@ -51,12 +52,13 @@ public class UpdateStorageLocation { } // 4. Speichern - switch (storageLocationRepository.save(location)) { - case Result.Failure(var err) -> - { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } - case Result.Success(var ignored) -> { } - } - - return Result.success(location); + return unitOfWork.executeAtomically(() -> { + switch (storageLocationRepository.save(location)) { + case Result.Failure(var err) -> + { return Result.failure(new StorageLocationError.RepositoryFailure(err.message())); } + case Result.Success(var ignored) -> { } + } + return Result.success(location); + }); } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java b/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java index 3fac362..a07aad3 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java +++ b/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java @@ -25,6 +25,7 @@ import de.effigenix.application.usermanagement.AuditLogger; import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StorageLocationRepository; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.AuthorizationPort; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -35,23 +36,23 @@ public class InventoryUseCaseConfiguration { // ==================== StorageLocation Use Cases ==================== @Bean - public CreateStorageLocation createStorageLocation(StorageLocationRepository storageLocationRepository) { - return new CreateStorageLocation(storageLocationRepository); + public CreateStorageLocation createStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) { + return new CreateStorageLocation(storageLocationRepository, unitOfWork); } @Bean - public UpdateStorageLocation updateStorageLocation(StorageLocationRepository storageLocationRepository) { - return new UpdateStorageLocation(storageLocationRepository); + public UpdateStorageLocation updateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) { + return new UpdateStorageLocation(storageLocationRepository, unitOfWork); } @Bean - public DeactivateStorageLocation deactivateStorageLocation(StorageLocationRepository storageLocationRepository, StockRepository stockRepository) { - return new DeactivateStorageLocation(storageLocationRepository, stockRepository); + public DeactivateStorageLocation deactivateStorageLocation(StorageLocationRepository storageLocationRepository, StockRepository stockRepository, UnitOfWork unitOfWork) { + return new DeactivateStorageLocation(storageLocationRepository, stockRepository, unitOfWork); } @Bean - public ActivateStorageLocation activateStorageLocation(StorageLocationRepository storageLocationRepository) { - return new ActivateStorageLocation(storageLocationRepository); + public ActivateStorageLocation activateStorageLocation(StorageLocationRepository storageLocationRepository, UnitOfWork unitOfWork) { + return new ActivateStorageLocation(storageLocationRepository, unitOfWork); } @Bean @@ -67,13 +68,13 @@ public class InventoryUseCaseConfiguration { // ==================== Stock Use Cases ==================== @Bean - public CreateStock createStock(StockRepository stockRepository) { - return new CreateStock(stockRepository); + public CreateStock createStock(StockRepository stockRepository, UnitOfWork unitOfWork) { + return new CreateStock(stockRepository, unitOfWork); } @Bean - public UpdateStock updateStock(StockRepository stockRepository) { - return new UpdateStock(stockRepository); + public UpdateStock updateStock(StockRepository stockRepository, UnitOfWork unitOfWork) { + return new UpdateStock(stockRepository, unitOfWork); } @Bean @@ -87,33 +88,33 @@ public class InventoryUseCaseConfiguration { } @Bean - public AddStockBatch addStockBatch(StockRepository stockRepository) { - return new AddStockBatch(stockRepository); + public AddStockBatch addStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) { + return new AddStockBatch(stockRepository, unitOfWork); } @Bean - public RemoveStockBatch removeStockBatch(StockRepository stockRepository) { - return new RemoveStockBatch(stockRepository); + public RemoveStockBatch removeStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) { + return new RemoveStockBatch(stockRepository, unitOfWork); } @Bean - public BlockStockBatch blockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) { - return new BlockStockBatch(stockRepository, auditLogger); + public BlockStockBatch blockStockBatch(StockRepository stockRepository, AuditLogger auditLogger, UnitOfWork unitOfWork) { + return new BlockStockBatch(stockRepository, auditLogger, unitOfWork); } @Bean - public UnblockStockBatch unblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger) { - return new UnblockStockBatch(stockRepository, auditLogger); + public UnblockStockBatch unblockStockBatch(StockRepository stockRepository, AuditLogger auditLogger, UnitOfWork unitOfWork) { + return new UnblockStockBatch(stockRepository, auditLogger, unitOfWork); } @Bean - public ReserveStock reserveStock(StockRepository stockRepository) { - return new ReserveStock(stockRepository); + public ReserveStock reserveStock(StockRepository stockRepository, UnitOfWork unitOfWork) { + return new ReserveStock(stockRepository, unitOfWork); } @Bean - public ReleaseReservation releaseReservation(StockRepository stockRepository) { - return new ReleaseReservation(stockRepository); + public ReleaseReservation releaseReservation(StockRepository stockRepository, UnitOfWork unitOfWork) { + return new ReleaseReservation(stockRepository, unitOfWork); } @Bean @@ -129,8 +130,8 @@ public class InventoryUseCaseConfiguration { // ==================== StockMovement Use Cases ==================== @Bean - public RecordStockMovement recordStockMovement(StockMovementRepository stockMovementRepository, AuthorizationPort authorizationPort) { - return new RecordStockMovement(stockMovementRepository, authorizationPort); + public RecordStockMovement recordStockMovement(StockMovementRepository stockMovementRepository, AuthorizationPort authorizationPort, UnitOfWork unitOfWork) { + return new RecordStockMovement(stockMovementRepository, authorizationPort, unitOfWork); } @Bean diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/ReservationEntity.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/ReservationEntity.java deleted file mode 100644 index d28fa34..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/ReservationEntity.java +++ /dev/null @@ -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 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 getAllocations() { return allocations; } - public void setAllocations(List allocations) { this.allocations = allocations; } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchAllocationEntity.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchAllocationEntity.java deleted file mode 100644 index 3f48023..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchAllocationEntity.java +++ /dev/null @@ -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; } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchEntity.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchEntity.java deleted file mode 100644 index db23081..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockBatchEntity.java +++ /dev/null @@ -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; } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockEntity.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockEntity.java deleted file mode 100644 index 655da46..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockEntity.java +++ /dev/null @@ -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 batches = new ArrayList<>(); - - @OneToMany(mappedBy = "stock", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - private List 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 getBatches() { return batches; } - public void setBatches(List batches) { this.batches = batches; } - - public List getReservations() { return reservations; } - public void setReservations(List reservations) { this.reservations = reservations; } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockMovementEntity.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockMovementEntity.java deleted file mode 100644 index 97ced4b..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockMovementEntity.java +++ /dev/null @@ -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; } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StorageLocationEntity.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StorageLocationEntity.java deleted file mode 100644 index a390b6a..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StorageLocationEntity.java +++ /dev/null @@ -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; } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMapper.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMapper.java deleted file mode 100644 index 64765d4..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMapper.java +++ /dev/null @@ -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 batchEntities = stock.batches().stream() - .map(b -> toBatchEntity(b, entity)) - .collect(Collectors.toList()); - entity.setBatches(batchEntities); - - List 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 batches = entity.getBatches().stream() - .map(this::toDomainBatch) - .collect(Collectors.toList()); - - List 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 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 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()) - ) - ); - } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMovementMapper.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMovementMapper.java deleted file mode 100644 index 7c7a3fa..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMovementMapper.java +++ /dev/null @@ -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() - ); - } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StorageLocationMapper.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StorageLocationMapper.java deleted file mode 100644 index 95ddf5d..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StorageLocationMapper.java +++ /dev/null @@ -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() - ); - } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java new file mode 100644 index 0000000..672b10c --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockMovementRepository.java @@ -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> 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> 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> 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> 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> 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> 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> 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> 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> 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 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) + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java new file mode 100644 index 0000000..235564a --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStockRepository.java @@ -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> 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> 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 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> 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> 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> 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> 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> 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 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 stockParams(Stock stock) { + var params = new java.util.LinkedHashMap(); + 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 loadChildrenForAll(List stocks) { + return stocks.stream() + .map(s -> loadChildren(s, s.id().value())) + .toList(); + } + + private List 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 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>(); + 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) {} +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java new file mode 100644 index 0000000..3c7073f --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JdbcStorageLocationRepository.java @@ -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> 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> 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> 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> 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 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 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 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 locationParams(StorageLocation location) { + var params = new java.util.LinkedHashMap(); + 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") + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockMovementRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockMovementRepository.java deleted file mode 100644 index 0865a63..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockMovementRepository.java +++ /dev/null @@ -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> findById(StockMovementId id) { - try { - Optional 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> findAll() { - try { - List 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> findAllByStockId(StockId stockId) { - try { - List 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> findAllByArticleId(ArticleId articleId) { - try { - List 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> findAllByMovementType(MovementType movementType) { - try { - List 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> findAllByBatchReference(String batchReference) { - try { - List 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> findAllByPerformedAtBetween(Instant from, Instant to) { - try { - List 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> findAllByPerformedAtAfter(Instant from) { - try { - List 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> findAllByPerformedAtBefore(Instant to) { - try { - List 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 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())); - } - } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockRepository.java deleted file mode 100644 index 66519a0..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockRepository.java +++ /dev/null @@ -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> findById(StockId id) { - try { - Optional 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> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { - try { - Optional 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 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> findAll() { - try { - List 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> findAllByStorageLocationId(StorageLocationId storageLocationId) { - try { - List 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> findAllByArticleId(ArticleId articleId) { - try { - List 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> findAllWithExpiryRelevantBatches(LocalDate referenceDate) { - try { - List 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> findAllBelowMinimumLevel() { - try { - List 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 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())); - } - } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java deleted file mode 100644 index 0962300..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStorageLocationRepository.java +++ /dev/null @@ -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> findById(StorageLocationId id) { - try { - Optional 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> findAll() { - try { - List 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> findByStorageType(StorageType storageType) { - try { - List 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> findActive() { - try { - List 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 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 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 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())); - } - } -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockJpaRepository.java deleted file mode 100644 index 228d797..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockJpaRepository.java +++ /dev/null @@ -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 { - - Optional findByArticleIdAndStorageLocationId(String articleId, String storageLocationId); - - boolean existsByArticleIdAndStorageLocationId(String articleId, String storageLocationId); - - List findAllByStorageLocationId(String storageLocationId); - - List 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 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 findAllBelowMinimumLevel(); -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockMovementJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockMovementJpaRepository.java deleted file mode 100644 index 757fe51..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockMovementJpaRepository.java +++ /dev/null @@ -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 { - - List findAllByStockId(String stockId); - - List findAllByArticleId(String articleId); - - List findAllByMovementType(String movementType); - - List findAllByBatchId(String batchId); - - List findAllByPerformedAtBetween(Instant from, Instant to); - - List findAllByPerformedAtGreaterThanEqual(Instant from); - - List findAllByPerformedAtLessThanEqual(Instant to); -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java deleted file mode 100644 index c04c09c..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StorageLocationJpaRepository.java +++ /dev/null @@ -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 { - - List findByStorageType(String storageType); - - List findByActiveTrue(); - - boolean existsByName(String name); - - boolean existsByNameAndIdNot(String name, String id); -} diff --git a/backend/src/test/java/de/effigenix/application/inventory/AddStockBatchTest.java b/backend/src/test/java/de/effigenix/application/inventory/AddStockBatchTest.java index 02c148d..4e51d8f 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/AddStockBatchTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/AddStockBatchTest.java @@ -6,6 +6,7 @@ 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 de.effigenix.shared.persistence.UnitOfWork; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,6 +29,7 @@ import static org.mockito.Mockito.*; class AddStockBatchTest { @Mock private StockRepository stockRepository; + @Mock private UnitOfWork unitOfWork; private AddStockBatch addStockBatch; private AddStockBatchCommand validCommand; @@ -35,7 +37,8 @@ class AddStockBatchTest { @BeforeEach 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( StockId.of("stock-1"), diff --git a/backend/src/test/java/de/effigenix/application/inventory/BlockStockBatchTest.java b/backend/src/test/java/de/effigenix/application/inventory/BlockStockBatchTest.java index e690451..d21873c 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/BlockStockBatchTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/BlockStockBatchTest.java @@ -7,6 +7,7 @@ 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 de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ class BlockStockBatchTest { @Mock private StockRepository stockRepository; @Mock private AuditLogger auditLogger; + @Mock private UnitOfWork unitOfWork; private BlockStockBatch blockStockBatch; private StockBatchId batchId; @@ -41,7 +43,8 @@ class BlockStockBatchTest { @BeforeEach 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"); var batch = StockBatch.reconstitute( diff --git a/backend/src/test/java/de/effigenix/application/inventory/DeactivateStorageLocationTest.java b/backend/src/test/java/de/effigenix/application/inventory/DeactivateStorageLocationTest.java index 09a7f1e..d575c7c 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/DeactivateStorageLocationTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/DeactivateStorageLocationTest.java @@ -4,6 +4,7 @@ import de.effigenix.domain.inventory.*; import de.effigenix.domain.masterdata.ArticleId; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; import org.junit.jupiter.api.BeforeEach; 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.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) @DisplayName("DeactivateStorageLocation Use Case") @@ -27,13 +27,15 @@ class DeactivateStorageLocationTest { @Mock private StorageLocationRepository storageLocationRepository; @Mock private StockRepository stockRepository; + @Mock private UnitOfWork unitOfWork; private DeactivateStorageLocation deactivateStorageLocation; private ActorId performedBy; @BeforeEach 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"); } diff --git a/backend/src/test/java/de/effigenix/application/inventory/RecordStockMovementTest.java b/backend/src/test/java/de/effigenix/application/inventory/RecordStockMovementTest.java index 2463186..4d8e51d 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/RecordStockMovementTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/RecordStockMovementTest.java @@ -4,6 +4,7 @@ import de.effigenix.application.inventory.command.RecordStockMovementCommand; import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; +import de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.AuthorizationPort; import org.junit.jupiter.api.BeforeEach; @@ -19,6 +20,7 @@ class RecordStockMovementTest { private StockMovementRepository repository; private AuthorizationPort authPort; + private UnitOfWork unitOfWork; private RecordStockMovement useCase; private final ActorId actor = ActorId.of("user-1"); @@ -26,7 +28,9 @@ class RecordStockMovementTest { void setUp() { repository = mock(StockMovementRepository.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); } diff --git a/backend/src/test/java/de/effigenix/application/inventory/ReleaseReservationTest.java b/backend/src/test/java/de/effigenix/application/inventory/ReleaseReservationTest.java index 5a15b34..a5ba561 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/ReleaseReservationTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/ReleaseReservationTest.java @@ -6,6 +6,7 @@ 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 de.effigenix.shared.persistence.UnitOfWork; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,6 +30,7 @@ import static org.mockito.Mockito.*; class ReleaseReservationTest { @Mock private StockRepository stockRepository; + @Mock private UnitOfWork unitOfWork; private ReleaseReservation releaseReservation; private Stock existingStock; @@ -36,7 +38,8 @@ class ReleaseReservationTest { @BeforeEach 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( StockBatchId.generate(), diff --git a/backend/src/test/java/de/effigenix/application/inventory/RemoveStockBatchTest.java b/backend/src/test/java/de/effigenix/application/inventory/RemoveStockBatchTest.java index 22bd9a3..aa6e2f0 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/RemoveStockBatchTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/RemoveStockBatchTest.java @@ -6,6 +6,7 @@ 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 de.effigenix.shared.persistence.UnitOfWork; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,6 +30,7 @@ import static org.mockito.Mockito.*; class RemoveStockBatchTest { @Mock private StockRepository stockRepository; + @Mock private UnitOfWork unitOfWork; private RemoveStockBatch removeStockBatch; private StockBatchId batchId; @@ -37,7 +39,8 @@ class RemoveStockBatchTest { @BeforeEach 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"); var batch = StockBatch.reconstitute( diff --git a/backend/src/test/java/de/effigenix/application/inventory/ReserveStockTest.java b/backend/src/test/java/de/effigenix/application/inventory/ReserveStockTest.java index 07513e7..372b3dc 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/ReserveStockTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/ReserveStockTest.java @@ -6,6 +6,7 @@ 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 de.effigenix.shared.persistence.UnitOfWork; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -29,6 +30,7 @@ import static org.mockito.Mockito.*; class ReserveStockTest { @Mock private StockRepository stockRepository; + @Mock private UnitOfWork unitOfWork; private ReserveStock reserveStock; private ReserveStockCommand validCommand; @@ -36,7 +38,8 @@ class ReserveStockTest { @BeforeEach 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( StockBatchId.generate(), diff --git a/backend/src/test/java/de/effigenix/application/inventory/UnblockStockBatchTest.java b/backend/src/test/java/de/effigenix/application/inventory/UnblockStockBatchTest.java index dc22c38..34e53ea 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/UnblockStockBatchTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/UnblockStockBatchTest.java @@ -7,6 +7,7 @@ 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 de.effigenix.shared.persistence.UnitOfWork; import de.effigenix.shared.security.ActorId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ class UnblockStockBatchTest { @Mock private StockRepository stockRepository; @Mock private AuditLogger auditLogger; + @Mock private UnitOfWork unitOfWork; private UnblockStockBatch unblockStockBatch; private StockBatchId batchId; @@ -41,7 +43,8 @@ class UnblockStockBatchTest { @BeforeEach 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"); var batch = StockBatch.reconstitute( diff --git a/backend/src/test/java/de/effigenix/application/inventory/UpdateStockTest.java b/backend/src/test/java/de/effigenix/application/inventory/UpdateStockTest.java index c4eb407..ab09ae7 100644 --- a/backend/src/test/java/de/effigenix/application/inventory/UpdateStockTest.java +++ b/backend/src/test/java/de/effigenix/application/inventory/UpdateStockTest.java @@ -5,6 +5,7 @@ import de.effigenix.domain.inventory.*; import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.common.UnitOfMeasure; +import de.effigenix.shared.persistence.UnitOfWork; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; 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.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) @DisplayName("UpdateStock Use Case") class UpdateStockTest { @Mock private StockRepository stockRepository; + @Mock private UnitOfWork unitOfWork; private UpdateStock updateStock; private Stock existingStock; @BeforeEach 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( StockId.of("stock-1"),