mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:39:57 +01:00
feat(inventory): Reservierung freigeben (#13)
DELETE /api/inventory/stocks/{stockId}/reservations/{reservationId}
gibt eine bestehende Reservierung frei und stellt die verfügbare
Menge wieder her. Zusätzlich Liquibase-Changeset 025 idempotent
gemacht (ON CONFLICT DO NOTHING).
This commit is contained in:
parent
0b49bb2977
commit
2938628db4
9 changed files with 601 additions and 25 deletions
|
|
@ -0,0 +1,46 @@
|
|||
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;
|
||||
|
||||
@Transactional
|
||||
public class ReleaseReservation {
|
||||
|
||||
private final StockRepository stockRepository;
|
||||
|
||||
public ReleaseReservation(StockRepository stockRepository) {
|
||||
this.stockRepository = stockRepository;
|
||||
}
|
||||
|
||||
public Result<StockError, Void> execute(ReleaseReservationCommand 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 freigeben
|
||||
switch (stock.releaseReservation(ReservationId.of(cmd.reservationId()))) {
|
||||
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||
case Result.Success(var ignored) -> { }
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package de.effigenix.application.inventory.command;
|
||||
|
||||
public record ReleaseReservationCommand(String stockId, String reservationId) {}
|
||||
|
|
@ -33,6 +33,7 @@ import java.util.stream.Collectors;
|
|||
* - availableQuantity: sum of AVAILABLE + EXPIRING_SOON batch quantities minus all reservation allocations
|
||||
* - 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
|
||||
* - reservations track allocated quantities per batch; no over-reservation possible
|
||||
*/
|
||||
public class Stock {
|
||||
|
|
@ -353,6 +354,18 @@ public class Stock {
|
|||
return Result.success(reservation);
|
||||
}
|
||||
|
||||
public Result<StockError, Void> releaseReservation(ReservationId reservationId) {
|
||||
Reservation reservation = reservations.stream()
|
||||
.filter(r -> r.id().equals(reservationId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (reservation == null) {
|
||||
return Result.failure(new StockError.ReservationNotFound(reservationId.value()));
|
||||
}
|
||||
this.reservations.remove(reservation);
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
// ==================== Expiry Management ====================
|
||||
|
||||
public Result<StockError, List<StockBatchId>> markExpiredBatches(LocalDate today) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import de.effigenix.application.inventory.UpdateStock;
|
|||
import de.effigenix.application.inventory.ListStocks;
|
||||
import de.effigenix.application.inventory.ListStocksBelowMinimum;
|
||||
import de.effigenix.application.inventory.RemoveStockBatch;
|
||||
import de.effigenix.application.inventory.ReleaseReservation;
|
||||
import de.effigenix.application.inventory.ReserveStock;
|
||||
import de.effigenix.application.inventory.UnblockStockBatch;
|
||||
import de.effigenix.application.inventory.CreateStorageLocation;
|
||||
|
|
@ -106,6 +107,11 @@ public class InventoryUseCaseConfiguration {
|
|||
return new ReserveStock(stockRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReleaseReservation releaseReservation(StockRepository stockRepository) {
|
||||
return new ReleaseReservation(stockRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CheckStockExpiry checkStockExpiry(StockRepository stockRepository) {
|
||||
return new CheckStockExpiry(stockRepository);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import de.effigenix.application.inventory.CreateStock;
|
|||
import de.effigenix.application.inventory.GetStock;
|
||||
import de.effigenix.application.inventory.ListStocks;
|
||||
import de.effigenix.application.inventory.ListStocksBelowMinimum;
|
||||
import de.effigenix.application.inventory.ReleaseReservation;
|
||||
import de.effigenix.application.inventory.RemoveStockBatch;
|
||||
import de.effigenix.application.inventory.ReserveStock;
|
||||
import de.effigenix.application.inventory.UnblockStockBatch;
|
||||
|
|
@ -14,6 +15,7 @@ import de.effigenix.application.inventory.command.AddStockBatchCommand;
|
|||
import de.effigenix.application.inventory.command.BlockStockBatchCommand;
|
||||
import de.effigenix.application.inventory.command.CreateStockCommand;
|
||||
import de.effigenix.application.inventory.command.RemoveStockBatchCommand;
|
||||
import de.effigenix.application.inventory.command.ReleaseReservationCommand;
|
||||
import de.effigenix.application.inventory.command.ReserveStockCommand;
|
||||
import de.effigenix.application.inventory.command.UnblockStockBatchCommand;
|
||||
import de.effigenix.application.inventory.command.UpdateStockCommand;
|
||||
|
|
@ -60,12 +62,13 @@ public class StockController {
|
|||
private final BlockStockBatch blockStockBatch;
|
||||
private final UnblockStockBatch unblockStockBatch;
|
||||
private final ReserveStock reserveStock;
|
||||
private final ReleaseReservation releaseReservation;
|
||||
|
||||
public StockController(CreateStock createStock, UpdateStock updateStock, GetStock getStock, ListStocks listStocks,
|
||||
ListStocksBelowMinimum listStocksBelowMinimum,
|
||||
AddStockBatch addStockBatch, RemoveStockBatch removeStockBatch,
|
||||
BlockStockBatch blockStockBatch, UnblockStockBatch unblockStockBatch,
|
||||
ReserveStock reserveStock) {
|
||||
ReserveStock reserveStock, ReleaseReservation releaseReservation) {
|
||||
this.createStock = createStock;
|
||||
this.updateStock = updateStock;
|
||||
this.getStock = getStock;
|
||||
|
|
@ -76,6 +79,7 @@ public class StockController {
|
|||
this.blockStockBatch = blockStockBatch;
|
||||
this.unblockStockBatch = unblockStockBatch;
|
||||
this.reserveStock = reserveStock;
|
||||
this.releaseReservation = releaseReservation;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -286,6 +290,26 @@ public class StockController {
|
|||
.body(ReservationResponse.from(result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{stockId}/reservations/{reservationId}")
|
||||
@PreAuthorize("hasAuthority('STOCK_WRITE')")
|
||||
public ResponseEntity<Void> releaseReservation(
|
||||
@PathVariable String stockId,
|
||||
@PathVariable String reservationId,
|
||||
Authentication authentication
|
||||
) {
|
||||
logger.info("Releasing reservation {} of stock {} by actor: {}", reservationId, stockId, authentication.getName());
|
||||
|
||||
var cmd = new ReleaseReservationCommand(stockId, reservationId);
|
||||
var result = releaseReservation.execute(cmd);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new StockDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Reservation {} of stock {} released", reservationId, stockId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
public static class StockDomainErrorException extends RuntimeException {
|
||||
private final StockError error;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,31 +6,16 @@
|
|||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet id="025-seed-production-order-permissions" author="effigenix">
|
||||
<comment>Add PRODUCTION_ORDER_READ and PRODUCTION_ORDER_WRITE permissions for ADMIN and PRODUCTION_MANAGER roles</comment>
|
||||
<comment>Add PRODUCTION_ORDER_READ and PRODUCTION_ORDER_WRITE permissions for ADMIN and PRODUCTION_MANAGER roles (idempotent)</comment>
|
||||
|
||||
<!-- ADMIN: PRODUCTION_ORDER_READ -->
|
||||
<insert tableName="role_permissions">
|
||||
<column name="role_id" value="c0a80121-0000-0000-0000-000000000001"/>
|
||||
<column name="permission" value="PRODUCTION_ORDER_READ"/>
|
||||
</insert>
|
||||
|
||||
<!-- ADMIN: PRODUCTION_ORDER_WRITE -->
|
||||
<insert tableName="role_permissions">
|
||||
<column name="role_id" value="c0a80121-0000-0000-0000-000000000001"/>
|
||||
<column name="permission" value="PRODUCTION_ORDER_WRITE"/>
|
||||
</insert>
|
||||
|
||||
<!-- PRODUCTION_MANAGER: PRODUCTION_ORDER_READ -->
|
||||
<insert tableName="role_permissions">
|
||||
<column name="role_id" value="c0a80121-0000-0000-0000-000000000002"/>
|
||||
<column name="permission" value="PRODUCTION_ORDER_READ"/>
|
||||
</insert>
|
||||
|
||||
<!-- PRODUCTION_MANAGER: PRODUCTION_ORDER_WRITE -->
|
||||
<insert tableName="role_permissions">
|
||||
<column name="role_id" value="c0a80121-0000-0000-0000-000000000002"/>
|
||||
<column name="permission" value="PRODUCTION_ORDER_WRITE"/>
|
||||
</insert>
|
||||
<sql>
|
||||
INSERT INTO role_permissions (role_id, permission) VALUES
|
||||
('c0a80121-0000-0000-0000-000000000001', 'PRODUCTION_ORDER_READ'),
|
||||
('c0a80121-0000-0000-0000-000000000001', 'PRODUCTION_ORDER_WRITE'),
|
||||
('c0a80121-0000-0000-0000-000000000002', 'PRODUCTION_ORDER_READ'),
|
||||
('c0a80121-0000-0000-0000-000000000002', 'PRODUCTION_ORDER_WRITE')
|
||||
ON CONFLICT DO NOTHING;
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue