mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 08:29:36 +01:00
fix(inventory): quantityUnit-Ermittlung bei heterogenen UoMs absichern (#60)
Stock.uniformUnitOfMeasure() gibt die UoM nur zurück wenn alle Chargen dieselbe Einheit haben, sonst Optional.empty(). StockResponse nutzt diese Methode statt blind die erste Charge zu nehmen.
This commit is contained in:
parent
004d96b291
commit
b9b89e3f0e
3 changed files with 96 additions and 10 deletions
|
|
@ -15,6 +15,7 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -446,6 +447,17 @@ public class Stock {
|
|||
|
||||
// ==================== Queries ====================
|
||||
|
||||
/**
|
||||
* Returns the uniform UoM if all batches share the same unit, empty otherwise.
|
||||
* Empty batches list also returns empty.
|
||||
*/
|
||||
public Optional<UnitOfMeasure> uniformUnitOfMeasure() {
|
||||
Set<UnitOfMeasure> units = batches.stream()
|
||||
.map(b -> b.quantity().uom())
|
||||
.collect(Collectors.toSet());
|
||||
return units.size() == 1 ? Optional.of(units.iterator().next()) : Optional.empty();
|
||||
}
|
||||
|
||||
public BigDecimal availableQuantity() {
|
||||
BigDecimal gross = batches.stream()
|
||||
.filter(b -> b.status() == StockBatchStatus.AVAILABLE || b.status() == StockBatchStatus.EXPIRING_SOON)
|
||||
|
|
@ -465,15 +477,17 @@ public class Stock {
|
|||
if (minimumLevel == null) {
|
||||
return false;
|
||||
}
|
||||
Set<UnitOfMeasure> batchUnits = batches.stream()
|
||||
.filter(b -> b.status() == StockBatchStatus.AVAILABLE || b.status() == StockBatchStatus.EXPIRING_SOON)
|
||||
.map(b -> b.quantity().uom())
|
||||
.collect(Collectors.toSet());
|
||||
UnitOfMeasure minimumUnit = minimumLevel.quantity().uom();
|
||||
if (!batchUnits.isEmpty() && !(batchUnits.size() == 1 && batchUnits.contains(minimumUnit))) {
|
||||
Optional<UnitOfMeasure> uniform = uniformUnitOfMeasure();
|
||||
if (uniform.isEmpty() && !batches.isEmpty()) {
|
||||
logger.log(System.Logger.Level.WARNING,
|
||||
"Unit mismatch in stock {0}: batch units {1} vs minimum level unit {2} — skipping below-minimum check",
|
||||
id, batchUnits, minimumUnit);
|
||||
"Mixed UoMs in stock {0} — skipping below-minimum check", id);
|
||||
return false;
|
||||
}
|
||||
if (uniform.isPresent() && uniform.get() != minimumUnit) {
|
||||
logger.log(System.Logger.Level.WARNING,
|
||||
"Unit mismatch in stock {0}: batch unit {1} vs minimum level unit {2} — skipping below-minimum check",
|
||||
id, uniform.get(), minimumUnit);
|
||||
return false;
|
||||
}
|
||||
return availableQuantity().compareTo(minimumLevel.quantity().amount()) < 0;
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ public record StockResponse(
|
|||
|
||||
BigDecimal availableQuantity = stock.availableQuantity();
|
||||
|
||||
String quantityUnit = stock.batches().isEmpty()
|
||||
? null
|
||||
: stock.batches().getFirst().quantity().uom().name();
|
||||
String quantityUnit = stock.uniformUnitOfMeasure()
|
||||
.map(Enum::name)
|
||||
.orElse(null);
|
||||
|
||||
List<ReservationResponse> reservationResponses = stock.reservations().stream()
|
||||
.map(ReservationResponse::from)
|
||||
|
|
|
|||
|
|
@ -2068,6 +2068,78 @@ class StockTest {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== uniformUnitOfMeasure ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("uniformUnitOfMeasure()")
|
||||
class UniformUnitOfMeasure {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty when no batches")
|
||||
void should_returnEmpty_when_noBatches() {
|
||||
var stock = Stock.reconstitute(
|
||||
StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
|
||||
null, null, List.of(), List.of()
|
||||
);
|
||||
|
||||
assertThat(stock.uniformUnitOfMeasure()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return UoM when all batches have same unit")
|
||||
void should_returnUoM_when_allBatchesSameUnit() {
|
||||
var batch1 = StockBatch.reconstitute(
|
||||
StockBatchId.generate(),
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
|
||||
LocalDate.of(2026, 12, 31), StockBatchStatus.AVAILABLE, Instant.now()
|
||||
);
|
||||
var batch2 = StockBatch.reconstitute(
|
||||
StockBatchId.generate(),
|
||||
new BatchReference("BATCH-002", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.KILOGRAM),
|
||||
LocalDate.of(2026, 12, 31), StockBatchStatus.BLOCKED, Instant.now()
|
||||
);
|
||||
var stock = Stock.reconstitute(
|
||||
StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
|
||||
null, null, new ArrayList<>(List.of(batch1, batch2)), List.of()
|
||||
);
|
||||
|
||||
assertThat(stock.uniformUnitOfMeasure()).contains(UnitOfMeasure.KILOGRAM);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty when batches have different units")
|
||||
void should_returnEmpty_when_heterogeneousUnits() {
|
||||
var kgBatch = StockBatch.reconstitute(
|
||||
StockBatchId.generate(),
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
|
||||
LocalDate.of(2026, 12, 31), StockBatchStatus.AVAILABLE, Instant.now()
|
||||
);
|
||||
var literBatch = StockBatch.reconstitute(
|
||||
StockBatchId.generate(),
|
||||
new BatchReference("BATCH-002", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.LITER),
|
||||
LocalDate.of(2026, 12, 31), StockBatchStatus.AVAILABLE, Instant.now()
|
||||
);
|
||||
var stock = Stock.reconstitute(
|
||||
StockId.generate(), ArticleId.of("article-1"), StorageLocationId.of("location-1"),
|
||||
null, null, new ArrayList<>(List.of(kgBatch, literBatch)), List.of()
|
||||
);
|
||||
|
||||
assertThat(stock.uniformUnitOfMeasure()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return UoM when single batch present")
|
||||
void should_returnUoM_when_singleBatch() {
|
||||
var stock = createStockWithBatch("10", UnitOfMeasure.PIECE, StockBatchStatus.AVAILABLE);
|
||||
|
||||
assertThat(stock.uniformUnitOfMeasure()).contains(UnitOfMeasure.PIECE);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private Stock createValidStock() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue