1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 11:59: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:
Sebastian Frick 2026-02-25 22:56:45 +01:00
parent 19f1cf16a1
commit 0b6028b967
11 changed files with 773 additions and 2 deletions

View file

@ -0,0 +1,248 @@
package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.ConfirmReservationCommand;
import de.effigenix.domain.inventory.*;
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;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("ConfirmReservation Use Case")
class ConfirmReservationTest {
@Mock private StockRepository stockRepository;
@Mock private StockMovementRepository stockMovementRepository;
@Mock private UnitOfWork unitOfWork;
private ConfirmReservation confirmReservation;
@BeforeEach
void setUp() {
confirmReservation = new ConfirmReservation(stockRepository, stockMovementRepository, unitOfWork);
lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv ->
((java.util.function.Supplier<?>) inv.getArgument(0)).get());
}
@Test
@DisplayName("should confirm reservation, save stock and create stock movements")
void shouldConfirmReservationSuccessfully() {
var stock = createStockWithReservation("PRODUCTION_ORDER", "PO-001", "10");
var reservationId = stock.reservations().getFirst().id().value();
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
when(stockRepository.save(any())).thenReturn(Result.success(null));
when(stockMovementRepository.save(any())).thenReturn(Result.success(null));
var result = confirmReservation.execute(
new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));
assertThat(result.isSuccess()).isTrue();
verify(stockRepository).save(stock);
var movementCaptor = ArgumentCaptor.forClass(StockMovement.class);
verify(stockMovementRepository).save(movementCaptor.capture());
var movement = movementCaptor.getValue();
assertThat(movement.movementType()).isEqualTo(MovementType.PRODUCTION_CONSUMPTION);
assertThat(movement.direction()).isEqualTo(MovementDirection.OUT);
assertThat(movement.quantity().amount()).isEqualByComparingTo(new BigDecimal("10"));
assertThat(movement.performedBy()).isEqualTo("admin");
}
@Test
@DisplayName("should derive SALE movement type from SALE_ORDER reference type")
void shouldDeriveSaleMovementType() {
var stock = createStockWithReservation("SALE_ORDER", "SO-001", "5");
var reservationId = stock.reservations().getFirst().id().value();
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
when(stockRepository.save(any())).thenReturn(Result.success(null));
when(stockMovementRepository.save(any())).thenReturn(Result.success(null));
var result = confirmReservation.execute(
new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));
assertThat(result.isSuccess()).isTrue();
var movementCaptor = ArgumentCaptor.forClass(StockMovement.class);
verify(stockMovementRepository).save(movementCaptor.capture());
assertThat(movementCaptor.getValue().movementType()).isEqualTo(MovementType.SALE);
}
@Test
@DisplayName("should create separate movements for multi-batch reservation")
void shouldCreateSeparateMovementsForMultiBatch() {
var batch1 = StockBatch.reconstitute(
StockBatchId.generate(),
new BatchReference("BATCH-001", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal("6"), UnitOfMeasure.KILOGRAM),
LocalDate.of(2026, 6, 30), StockBatchStatus.AVAILABLE, Instant.now()
);
var batch2 = StockBatch.reconstitute(
StockBatchId.generate(),
new BatchReference("BATCH-002", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal("8"), UnitOfMeasure.KILOGRAM),
LocalDate.of(2026, 12, 31), StockBatchStatus.AVAILABLE, Instant.now()
);
var stock = Stock.reconstitute(
StockId.of("stock-1"),
de.effigenix.domain.masterdata.ArticleId.of("article-1"),
StorageLocationId.of("location-1"),
null, null, new ArrayList<>(List.of(batch1, batch2)), List.of()
);
stock.reserve(new ReservationDraft("PRODUCTION_ORDER", "PO-001", "10", "KILOGRAM", "NORMAL"));
var reservationId = stock.reservations().getFirst().id().value();
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
when(stockRepository.save(any())).thenReturn(Result.success(null));
when(stockMovementRepository.save(any())).thenReturn(Result.success(null));
var result = confirmReservation.execute(
new ConfirmReservationCommand("stock-1", reservationId, "admin"));
assertThat(result.isSuccess()).isTrue();
verify(stockMovementRepository, times(2)).save(any());
}
@Test
@DisplayName("should fail with StockNotFound when stock does not exist")
void shouldFailWhenStockNotFound() {
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.empty()));
var result = confirmReservation.execute(
new ConfirmReservationCommand("stock-1", "res-1", "admin"));
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.StockNotFound.class);
verify(stockRepository, never()).save(any());
verify(stockMovementRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ReservationNotFound when reservation does not exist")
void shouldFailWhenReservationNotFound() {
var stock = createStockWithBatch("20");
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
var result = confirmReservation.execute(
new ConfirmReservationCommand(stock.id().value(), "nonexistent", "admin"));
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.ReservationNotFound.class);
verify(stockRepository, never()).save(any());
verify(stockMovementRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
when(stockRepository.findById(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = confirmReservation.execute(
new ConfirmReservationCommand("stock-1", "res-1", "admin"));
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
verify(stockRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var stock = createStockWithReservation("PRODUCTION_ORDER", "PO-001", "10");
var reservationId = stock.reservations().getFirst().id().value();
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
when(stockRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = confirmReservation.execute(
new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when stockMovementRepository save fails")
void shouldFailWhenStockMovementSaveFails() {
var stock = createStockWithReservation("PRODUCTION_ORDER", "PO-001", "10");
var reservationId = stock.reservations().getFirst().id().value();
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
when(stockRepository.save(any())).thenReturn(Result.success(null));
when(stockMovementRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = confirmReservation.execute(
new ConfirmReservationCommand(stock.id().value(), reservationId, "admin"));
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
}
@Test
@DisplayName("should confirm only targeted reservation when stock has multiple")
void shouldConfirmOnlyTargetedReservation() {
var stock = createStockWithBatch("50");
stock.reserve(new ReservationDraft("PRODUCTION_ORDER", "PO-001", "10", "KILOGRAM", "NORMAL"));
stock.reserve(new ReservationDraft("SALE_ORDER", "SO-001", "5", "KILOGRAM", "URGENT"));
assertThat(stock.reservations()).hasSize(2);
var firstReservationId = stock.reservations().getFirst().id().value();
when(stockRepository.findById(any())).thenReturn(Result.success(Optional.of(stock)));
when(stockRepository.save(any())).thenReturn(Result.success(null));
when(stockMovementRepository.save(any())).thenReturn(Result.success(null));
var result = confirmReservation.execute(
new ConfirmReservationCommand(stock.id().value(), firstReservationId, "admin"));
assertThat(result.isSuccess()).isTrue();
assertThat(stock.reservations()).hasSize(1);
assertThat(stock.reservations().getFirst().referenceId()).isEqualTo("SO-001");
}
// ==================== Helpers ====================
private Stock createStockWithBatch(String amount) {
var batch = StockBatch.reconstitute(
StockBatchId.generate(),
new BatchReference("BATCH-001", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal(amount), UnitOfMeasure.KILOGRAM),
LocalDate.of(2026, 12, 31),
StockBatchStatus.AVAILABLE,
Instant.now()
);
return Stock.reconstitute(
StockId.of("stock-1"),
de.effigenix.domain.masterdata.ArticleId.of("article-1"),
StorageLocationId.of("location-1"),
null, null, new ArrayList<>(List.of(batch)), List.of()
);
}
private Stock createStockWithReservation(String referenceType, String referenceId, String amount) {
var stock = createStockWithBatch("50");
stock.reserve(new ReservationDraft(referenceType, referenceId, amount, "KILOGRAM", "NORMAL"));
return stock;
}
}

View file

@ -1788,6 +1788,126 @@ class StockTest {
}
}
// ==================== confirmReservation ====================
@Nested
@DisplayName("confirmReservation()")
class ConfirmReservation {
@Test
@DisplayName("should deduct allocated quantities and remove reservation")
void shouldDeductAllocatedQuantitiesAndRemoveReservation() {
var stock = createStockWithBatchAndExpiry("20", UnitOfMeasure.KILOGRAM,
StockBatchStatus.AVAILABLE, LocalDate.of(2026, 12, 31));
var batchId = stock.batches().getFirst().id();
var reserveResult = stock.reserve(new ReservationDraft("PRODUCTION_ORDER", "PO-001", "8", "KILOGRAM", "NORMAL"));
assertThat(reserveResult.isSuccess()).isTrue();
var reservationId = reserveResult.unsafeGetValue().id();
var result = stock.confirmReservation(reservationId);
assertThat(result.isSuccess()).isTrue();
var confirmed = result.unsafeGetValue();
assertThat(confirmed.referenceType()).isEqualTo(ReferenceType.PRODUCTION_ORDER);
assertThat(confirmed.referenceId()).isEqualTo("PO-001");
assertThat(confirmed.allocations()).hasSize(1);
assertThat(confirmed.allocations().getFirst().stockBatchId()).isEqualTo(batchId);
assertThat(confirmed.allocations().getFirst().allocatedQuantity().amount())
.isEqualByComparingTo(new BigDecimal("8"));
assertThat(stock.reservations()).isEmpty();
assertThat(stock.batches()).hasSize(1);
assertThat(stock.batches().getFirst().quantity().amount())
.isEqualByComparingTo(new BigDecimal("12"));
}
@Test
@DisplayName("should deduct from multiple batches correctly")
void shouldDeductFromMultipleBatches() {
var batch1 = StockBatch.reconstitute(
StockBatchId.generate(),
new BatchReference("BATCH-001", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal("6"), UnitOfMeasure.KILOGRAM),
LocalDate.of(2026, 6, 30), StockBatchStatus.AVAILABLE, Instant.now()
);
var batch2 = StockBatch.reconstitute(
StockBatchId.generate(),
new BatchReference("BATCH-002", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal("8"), UnitOfMeasure.KILOGRAM),
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(batch1, batch2)), List.of()
);
// Reserve 10kg: 6kg from batch1 (FEFO) + 4kg from batch2
var reserveResult = stock.reserve(new ReservationDraft("SALE_ORDER", "SO-001", "10", "KILOGRAM", "NORMAL"));
assertThat(reserveResult.isSuccess()).isTrue();
assertThat(reserveResult.unsafeGetValue().allocations()).hasSize(2);
var result = stock.confirmReservation(reserveResult.unsafeGetValue().id());
assertThat(result.isSuccess()).isTrue();
var confirmed = result.unsafeGetValue();
assertThat(confirmed.referenceType()).isEqualTo(ReferenceType.SALE_ORDER);
assertThat(confirmed.allocations()).hasSize(2);
assertThat(stock.reservations()).isEmpty();
// batch1 fully consumed removed, batch2 has 4kg remaining
assertThat(stock.batches()).hasSize(1);
assertThat(stock.batches().getFirst().quantity().amount())
.isEqualByComparingTo(new BigDecimal("4"));
}
@Test
@DisplayName("should remove batch when quantity reaches zero")
void shouldRemoveBatchWhenQuantityZero() {
var stock = createStockWithBatchAndExpiry("10", UnitOfMeasure.KILOGRAM,
StockBatchStatus.AVAILABLE, LocalDate.of(2026, 12, 31));
var reserveResult = stock.reserve(new ReservationDraft("PRODUCTION_ORDER", "PO-001", "10", "KILOGRAM", "NORMAL"));
assertThat(reserveResult.isSuccess()).isTrue();
var result = stock.confirmReservation(reserveResult.unsafeGetValue().id());
assertThat(result.isSuccess()).isTrue();
assertThat(stock.batches()).isEmpty();
assertThat(stock.reservations()).isEmpty();
}
@Test
@DisplayName("should fail with ReservationNotFound when reservation does not exist")
void shouldFailWhenReservationNotFound() {
var stock = createValidStock();
var result = stock.confirmReservation(ReservationId.of("nonexistent"));
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.ReservationNotFound.class);
}
@Test
@DisplayName("should return correct batch references in confirmed allocations")
void shouldReturnCorrectBatchReferences() {
var batchRef = new BatchReference("MY-BATCH-42", BatchType.PURCHASED);
var batch = StockBatch.reconstitute(
StockBatchId.generate(), batchRef,
Quantity.reconstitute(new BigDecimal("20"), UnitOfMeasure.KILOGRAM),
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(batch)), List.of()
);
var reserveResult = stock.reserve(new ReservationDraft("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL"));
var result = stock.confirmReservation(reserveResult.unsafeGetValue().id());
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().allocations().getFirst().batchReference()).isEqualTo(batchRef);
}
}
// ==================== releaseReservation ====================
@Nested

View file

@ -27,6 +27,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* - Story 2.1 Bestandsposition anlegen
* - Story 2.2 Charge einbuchen (addBatch)
* - Story 2.5 Bestandsposition und Chargen abfragen
* - Story 4.3 Reservierung bestätigen (Material entnehmen)
*/
@DisplayName("Stock Controller Integration Tests")
class StockControllerIntegrationTest extends AbstractIntegrationTest {
@ -1480,6 +1481,175 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
}
}
// ==================== Reservierung bestätigen (confirmReservation) ====================
@Nested
@DisplayName("POST /{stockId}/reservations/{reservationId}/confirm Reservierung bestätigen")
class ConfirmReservationEndpoint {
@Test
@DisplayName("Reservierung bestätigen → 200, Menge abgezogen, Reservation entfernt")
void confirmReservation_returns200() throws Exception {
String stockId = createStock();
addBatchToStock(stockId); // 10 KILOGRAM
// Reservieren: 5 kg
var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");
var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
.header("Authorization", "Bearer " + adminToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andReturn();
String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();
// Bestätigen
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk());
// Prüfen: Reservation entfernt, Menge physisch abgezogen
mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
.header("Authorization", "Bearer " + adminToken))
.andExpect(jsonPath("$.reservations.length()").value(0))
.andExpect(jsonPath("$.batches[0].quantityAmount").value(5))
.andExpect(jsonPath("$.availableQuantity").value(5));
}
@Test
@DisplayName("Bestätigung der gesamten Charge → Batch wird entfernt")
void confirmReservation_fullBatch_removesBatch() throws Exception {
String stockId = createStock();
addBatchToStock(stockId); // 10 KILOGRAM
// Alles reservieren
var request = new ReserveStockRequest("SALE_ORDER", "SO-001", "10", "KILOGRAM", "NORMAL");
var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
.header("Authorization", "Bearer " + adminToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andReturn();
String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();
// Bestätigen
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk());
// Batch entfernt
mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
.header("Authorization", "Bearer " + adminToken))
.andExpect(jsonPath("$.reservations.length()").value(0))
.andExpect(jsonPath("$.batches.length()").value(0))
.andExpect(jsonPath("$.availableQuantity").value(0));
}
@Test
@DisplayName("Reservierung nicht gefunden → 404")
void confirmReservation_notFound_returns404() throws Exception {
String stockId = createStock();
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
stockId, UUID.randomUUID().toString())
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("RESERVATION_NOT_FOUND"));
}
@Test
@DisplayName("Stock nicht gefunden → 404")
void confirmReservation_stockNotFound_returns404() throws Exception {
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
UUID.randomUUID().toString(), UUID.randomUUID().toString())
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
}
@Test
@DisplayName("Ohne STOCK_WRITE → 403")
void confirmReservation_withViewerToken_returns403() throws Exception {
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
UUID.randomUUID().toString(), UUID.randomUUID().toString())
.header("Authorization", "Bearer " + viewerToken))
.andExpect(status().isForbidden());
}
@Test
@DisplayName("Ohne Token → 401")
void confirmReservation_withoutToken_returns401() throws Exception {
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
UUID.randomUUID().toString(), UUID.randomUUID().toString()))
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("Idempotenz: zweites Confirm auf gleiche Reservierung → 404")
void confirmReservation_idempotent_returns404OnSecondCall() throws Exception {
String stockId = createStock();
addBatchToStock(stockId);
var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");
var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
.header("Authorization", "Bearer " + adminToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andReturn();
String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();
// Erstes Confirm 200
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk());
// Zweites Confirm 404
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("RESERVATION_NOT_FOUND"));
}
@Test
@DisplayName("Mehrere Reservierungen: eine bestätigen, andere bleibt bestehen")
void confirmReservation_multipleReservations_keepsOthers() throws Exception {
String stockId = createStock();
addBatchToStock(stockId); // 10 KILOGRAM
// Reservierung 1: 4 kg
var req1 = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "4", "KILOGRAM", "NORMAL");
var res1 = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
.header("Authorization", "Bearer " + adminToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req1)))
.andExpect(status().isCreated())
.andReturn();
String reservationId1 = objectMapper.readTree(res1.getResponse().getContentAsString()).get("id").asText();
// Reservierung 2: 3 kg
var req2 = new ReserveStockRequest("SALE_ORDER", "SO-001", "3", "KILOGRAM", "URGENT");
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
.header("Authorization", "Bearer " + adminToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req2)))
.andExpect(status().isCreated());
// Erste Reservierung bestätigen
mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId1)
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk());
// Batch: 10 - 4 = 6 kg, SO-001 bleibt
mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
.header("Authorization", "Bearer " + adminToken))
.andExpect(jsonPath("$.batches[0].quantityAmount").value(6))
.andExpect(jsonPath("$.reservations.length()").value(1))
.andExpect(jsonPath("$.reservations[0].referenceId").value("SO-001"))
.andExpect(jsonPath("$.availableQuantity").value(3));
}
}
// ==================== Hilfsmethoden ====================
private String createStorageLocation() throws Exception {