mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 15:49:35 +01:00
feat(inventory): Reservierung bestätigen – Material entnehmen (US-4.3)
Allokierte Mengen werden physisch aus den Chargen abgezogen, StockMovements erzeugt und die Reservation entfernt. MovementType wird aus ReferenceType abgeleitet (PRODUCTION_ORDER→PRODUCTION_CONSUMPTION, SALE_ORDER→SALE).
This commit is contained in:
parent
19f1cf16a1
commit
0b6028b967
11 changed files with 773 additions and 2 deletions
|
|
@ -0,0 +1,93 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.application.inventory.command.ConfirmReservationCommand;
|
||||
import de.effigenix.domain.inventory.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.persistence.UnitOfWork;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ConfirmReservation {
|
||||
|
||||
private final StockRepository stockRepository;
|
||||
private final StockMovementRepository stockMovementRepository;
|
||||
private final UnitOfWork unitOfWork;
|
||||
|
||||
public ConfirmReservation(StockRepository stockRepository,
|
||||
StockMovementRepository stockMovementRepository,
|
||||
UnitOfWork unitOfWork) {
|
||||
this.stockRepository = stockRepository;
|
||||
this.stockMovementRepository = stockMovementRepository;
|
||||
this.unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public Result<StockError, Void> execute(ConfirmReservationCommand cmd) {
|
||||
// 1. Stock laden
|
||||
Stock stock;
|
||||
switch (stockRepository.findById(StockId.of(cmd.stockId()))) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new StockError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var opt) -> {
|
||||
if (opt.isEmpty()) {
|
||||
return Result.failure(new StockError.StockNotFound(cmd.stockId()));
|
||||
}
|
||||
stock = opt.get();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Reservation bestätigen
|
||||
ConfirmedReservation confirmed;
|
||||
switch (stock.confirmReservation(ReservationId.of(cmd.reservationId()))) {
|
||||
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||
case Result.Success(var val) -> confirmed = val;
|
||||
}
|
||||
|
||||
// 3. MovementType aus ReferenceType ableiten
|
||||
String movementType = switch (confirmed.referenceType()) {
|
||||
case PRODUCTION_ORDER -> "PRODUCTION_CONSUMPTION";
|
||||
case SALE_ORDER -> "SALE";
|
||||
};
|
||||
|
||||
// 4. StockMovements erzeugen
|
||||
List<StockMovement> movements = new ArrayList<>();
|
||||
for (ConfirmedAllocation alloc : confirmed.allocations()) {
|
||||
var draft = new StockMovementDraft(
|
||||
stock.id().value(),
|
||||
stock.articleId().value(),
|
||||
alloc.stockBatchId().value(),
|
||||
alloc.batchReference().batchId(),
|
||||
alloc.batchReference().batchType().name(),
|
||||
movementType,
|
||||
null,
|
||||
alloc.allocatedQuantity().amount().toPlainString(),
|
||||
alloc.allocatedQuantity().uom().name(),
|
||||
null,
|
||||
null,
|
||||
cmd.performedBy()
|
||||
);
|
||||
switch (StockMovement.record(draft)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new StockError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var val) -> movements.add(val);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Atomar speichern
|
||||
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) -> { }
|
||||
}
|
||||
for (StockMovement movement : movements) {
|
||||
switch (stockMovementRepository.save(movement)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new StockError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var ignored) -> { }
|
||||
}
|
||||
}
|
||||
return Result.success(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package de.effigenix.application.inventory.command;
|
||||
|
||||
public record ConfirmReservationCommand(String stockId, String reservationId, String performedBy) {}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package de.effigenix.domain.inventory;
|
||||
|
||||
import de.effigenix.shared.common.Quantity;
|
||||
|
||||
public record ConfirmedAllocation(
|
||||
StockBatchId stockBatchId,
|
||||
BatchReference batchReference,
|
||||
Quantity allocatedQuantity
|
||||
) {}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package de.effigenix.domain.inventory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ConfirmedReservation(
|
||||
ReferenceType referenceType,
|
||||
String referenceId,
|
||||
List<ConfirmedAllocation> allocations
|
||||
) {}
|
||||
|
|
@ -34,6 +34,8 @@ import java.util.stream.Collectors;
|
|||
* - isBelowMinimumLevel: true when minimumLevel is set and availableQuantity < minimumLevel amount
|
||||
* - reserve: FEFO allocation across AVAILABLE/EXPIRING_SOON batches sorted by expiryDate ASC
|
||||
* - releaseReservation: removes reservation by ID, implicitly freeing allocated quantities
|
||||
* - confirmReservation: deducts allocated quantities from batches, removes reservation, returns ConfirmedReservation
|
||||
* - batches with quantity 0 after deduction are removed
|
||||
* - reservations track allocated quantities per batch; no over-reservation possible
|
||||
*/
|
||||
public class Stock {
|
||||
|
|
@ -354,6 +356,61 @@ public class Stock {
|
|||
return Result.success(reservation);
|
||||
}
|
||||
|
||||
public Result<StockError, ConfirmedReservation> confirmReservation(ReservationId reservationId) {
|
||||
// 1. Reservation finden
|
||||
Reservation reservation = reservations.stream()
|
||||
.filter(r -> r.id().equals(reservationId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (reservation == null) {
|
||||
return Result.failure(new StockError.ReservationNotFound(reservationId.value()));
|
||||
}
|
||||
|
||||
// 2. Für jede Allocation: Batch finden und BatchReference merken
|
||||
List<ConfirmedAllocation> confirmedAllocations = new ArrayList<>();
|
||||
for (StockBatchAllocation alloc : reservation.allocations()) {
|
||||
StockBatch batch = batches.stream()
|
||||
.filter(b -> b.id().equals(alloc.stockBatchId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (batch == null) {
|
||||
return Result.failure(new StockError.BatchNotFound(alloc.stockBatchId().value()));
|
||||
}
|
||||
confirmedAllocations.add(new ConfirmedAllocation(
|
||||
batch.id(), batch.batchReference(), alloc.allocatedQuantity()));
|
||||
}
|
||||
|
||||
// 3. Für jede Allocation: Menge abziehen, bei 0 Batch entfernen
|
||||
for (ConfirmedAllocation confirmed : confirmedAllocations) {
|
||||
StockBatch batch = batches.stream()
|
||||
.filter(b -> b.id().equals(confirmed.stockBatchId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (batch == null) {
|
||||
return Result.failure(new StockError.BatchNotFound(confirmed.stockBatchId().value()));
|
||||
}
|
||||
|
||||
Quantity remaining;
|
||||
switch (batch.removeQuantity(confirmed.allocatedQuantity())) {
|
||||
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||
case Result.Success(var val) -> remaining = val;
|
||||
}
|
||||
|
||||
if (remaining.amount().signum() == 0) {
|
||||
this.batches.remove(batch);
|
||||
} else {
|
||||
int index = this.batches.indexOf(batch);
|
||||
this.batches.set(index, batch.withQuantity(remaining));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Reservation entfernen
|
||||
this.reservations.remove(reservation);
|
||||
|
||||
return Result.success(new ConfirmedReservation(
|
||||
reservation.referenceType(), reservation.referenceId(), confirmedAllocations));
|
||||
}
|
||||
|
||||
public Result<StockError, Void> releaseReservation(ReservationId reservationId) {
|
||||
Reservation reservation = reservations.stream()
|
||||
.filter(r -> r.id().equals(reservationId))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package de.effigenix.infrastructure.config;
|
|||
|
||||
import de.effigenix.application.inventory.GetStockMovement;
|
||||
import de.effigenix.application.inventory.ListStockMovements;
|
||||
import de.effigenix.application.inventory.ConfirmReservation;
|
||||
import de.effigenix.application.inventory.RecordStockMovement;
|
||||
import de.effigenix.application.inventory.ActivateStorageLocation;
|
||||
import de.effigenix.application.inventory.AddStockBatch;
|
||||
|
|
@ -117,6 +118,13 @@ public class InventoryUseCaseConfiguration {
|
|||
return new ReleaseReservation(stockRepository, unitOfWork);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ConfirmReservation confirmReservation(StockRepository stockRepository,
|
||||
StockMovementRepository stockMovementRepository,
|
||||
UnitOfWork unitOfWork) {
|
||||
return new ConfirmReservation(stockRepository, stockMovementRepository, unitOfWork);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CheckStockExpiry checkStockExpiry(StockRepository stockRepository) {
|
||||
return new CheckStockExpiry(stockRepository);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package de.effigenix.infrastructure.inventory.web.controller;
|
|||
|
||||
import de.effigenix.application.inventory.AddStockBatch;
|
||||
import de.effigenix.application.inventory.BlockStockBatch;
|
||||
import de.effigenix.application.inventory.ConfirmReservation;
|
||||
import de.effigenix.application.inventory.CreateStock;
|
||||
import de.effigenix.application.inventory.GetStock;
|
||||
import de.effigenix.application.inventory.ListStocks;
|
||||
|
|
@ -12,6 +13,7 @@ import de.effigenix.application.inventory.ReserveStock;
|
|||
import de.effigenix.application.inventory.UnblockStockBatch;
|
||||
import de.effigenix.application.inventory.UpdateStock;
|
||||
import de.effigenix.application.inventory.command.AddStockBatchCommand;
|
||||
import de.effigenix.application.inventory.command.ConfirmReservationCommand;
|
||||
import de.effigenix.application.inventory.command.BlockStockBatchCommand;
|
||||
import de.effigenix.application.inventory.command.CreateStockCommand;
|
||||
import de.effigenix.application.inventory.command.RemoveStockBatchCommand;
|
||||
|
|
@ -63,12 +65,14 @@ public class StockController {
|
|||
private final UnblockStockBatch unblockStockBatch;
|
||||
private final ReserveStock reserveStock;
|
||||
private final ReleaseReservation releaseReservation;
|
||||
private final ConfirmReservation confirmReservation;
|
||||
|
||||
public StockController(CreateStock createStock, UpdateStock updateStock, GetStock getStock, ListStocks listStocks,
|
||||
ListStocksBelowMinimum listStocksBelowMinimum,
|
||||
AddStockBatch addStockBatch, RemoveStockBatch removeStockBatch,
|
||||
BlockStockBatch blockStockBatch, UnblockStockBatch unblockStockBatch,
|
||||
ReserveStock reserveStock, ReleaseReservation releaseReservation) {
|
||||
ReserveStock reserveStock, ReleaseReservation releaseReservation,
|
||||
ConfirmReservation confirmReservation) {
|
||||
this.createStock = createStock;
|
||||
this.updateStock = updateStock;
|
||||
this.getStock = getStock;
|
||||
|
|
@ -80,6 +84,7 @@ public class StockController {
|
|||
this.unblockStockBatch = unblockStockBatch;
|
||||
this.reserveStock = reserveStock;
|
||||
this.releaseReservation = releaseReservation;
|
||||
this.confirmReservation = confirmReservation;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -310,6 +315,26 @@ public class StockController {
|
|||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{stockId}/reservations/{reservationId}/confirm")
|
||||
@PreAuthorize("hasAuthority('STOCK_WRITE')")
|
||||
public ResponseEntity<Void> confirmReservation(
|
||||
@PathVariable String stockId,
|
||||
@PathVariable String reservationId,
|
||||
Authentication authentication
|
||||
) {
|
||||
logger.info("Confirming reservation {} of stock {} by actor: {}", reservationId, stockId, authentication.getName());
|
||||
|
||||
var cmd = new ConfirmReservationCommand(stockId, reservationId, authentication.getName());
|
||||
var result = confirmReservation.execute(cmd);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new StockDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Reservation {} of stock {} confirmed", reservationId, stockId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
public static class StockDomainErrorException extends RuntimeException {
|
||||
private final StockError error;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue