mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:09:35 +01:00
feat(inventory): Charge sperren/entsperren (blockBatch/unblockBatch) (#7)
Gesperrte Chargen können nicht entnommen oder reserviert werden. blockBatch: AVAILABLE/EXPIRING_SOON → BLOCKED; unblockBatch: BLOCKED → AVAILABLE/EXPIRING_SOON (MHD-Check).
This commit is contained in:
parent
8a9d2bfc30
commit
e7c3258f07
15 changed files with 934 additions and 1 deletions
|
|
@ -0,0 +1,46 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.application.inventory.command.BlockStockBatchCommand;
|
||||
import de.effigenix.domain.inventory.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional
|
||||
public class BlockStockBatch {
|
||||
|
||||
private final StockRepository stockRepository;
|
||||
|
||||
public BlockStockBatch(StockRepository stockRepository) {
|
||||
this.stockRepository = stockRepository;
|
||||
}
|
||||
|
||||
public Result<StockError, Void> execute(BlockStockBatchCommand 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. Batch sperren (Domain validiert)
|
||||
switch (stock.blockBatch(StockBatchId.of(cmd.batchId()))) {
|
||||
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||
case Result.Success(var ignored) -> { }
|
||||
}
|
||||
|
||||
// 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(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.application.inventory.command.UnblockStockBatchCommand;
|
||||
import de.effigenix.domain.inventory.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Transactional
|
||||
public class UnblockStockBatch {
|
||||
|
||||
private final StockRepository stockRepository;
|
||||
|
||||
public UnblockStockBatch(StockRepository stockRepository) {
|
||||
this.stockRepository = stockRepository;
|
||||
}
|
||||
|
||||
public Result<StockError, Void> execute(UnblockStockBatchCommand 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. Batch entsperren (Domain validiert)
|
||||
switch (stock.unblockBatch(StockBatchId.of(cmd.batchId()))) {
|
||||
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||
case Result.Success(var ignored) -> { }
|
||||
}
|
||||
|
||||
// 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(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package de.effigenix.application.inventory.command;
|
||||
|
||||
public record BlockStockBatchCommand(
|
||||
String stockId,
|
||||
String batchId,
|
||||
String reason
|
||||
) {}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package de.effigenix.application.inventory.command;
|
||||
|
||||
public record UnblockStockBatchCommand(
|
||||
String stockId,
|
||||
String batchId
|
||||
) {}
|
||||
|
|
@ -4,6 +4,7 @@ import de.effigenix.domain.masterdata.ArticleId;
|
|||
import de.effigenix.shared.common.Quantity;
|
||||
import de.effigenix.shared.common.Result;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -17,6 +18,8 @@ import java.util.Objects;
|
|||
* - MinimumLevel optional (quantity amount >= 0)
|
||||
* - MinimumShelfLife optional (days > 0)
|
||||
* - BatchReference (batchId + batchType) unique within batches
|
||||
* - blockBatch: AVAILABLE/EXPIRING_SOON → BLOCKED; EXPIRED → not allowed; already BLOCKED → error
|
||||
* - unblockBatch: BLOCKED → AVAILABLE or EXPIRING_SOON (based on MHD check); not BLOCKED → error
|
||||
*/
|
||||
public class Stock {
|
||||
|
||||
|
|
@ -150,6 +153,51 @@ public class Stock {
|
|||
return Result.success(null);
|
||||
}
|
||||
|
||||
public Result<StockError, Void> blockBatch(StockBatchId batchId) {
|
||||
StockBatch batch = batches.stream()
|
||||
.filter(b -> b.id().equals(batchId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (batch == null) {
|
||||
return Result.failure(new StockError.BatchNotFound(batchId.value()));
|
||||
}
|
||||
if (batch.status() == StockBatchStatus.BLOCKED) {
|
||||
return Result.failure(new StockError.BatchAlreadyBlocked(batchId.value()));
|
||||
}
|
||||
if (batch.status() == StockBatchStatus.EXPIRED) {
|
||||
return Result.failure(new StockError.BatchNotAvailable(batchId.value(), batch.status().name()));
|
||||
}
|
||||
|
||||
int index = this.batches.indexOf(batch);
|
||||
this.batches.set(index, batch.withStatus(StockBatchStatus.BLOCKED));
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
public Result<StockError, Void> unblockBatch(StockBatchId batchId) {
|
||||
StockBatch batch = batches.stream()
|
||||
.filter(b -> b.id().equals(batchId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (batch == null) {
|
||||
return Result.failure(new StockError.BatchNotFound(batchId.value()));
|
||||
}
|
||||
if (batch.status() != StockBatchStatus.BLOCKED) {
|
||||
return Result.failure(new StockError.BatchNotBlocked(batchId.value()));
|
||||
}
|
||||
|
||||
StockBatchStatus newStatus = StockBatchStatus.AVAILABLE;
|
||||
if (minimumShelfLife != null) {
|
||||
LocalDate threshold = LocalDate.now().plusDays(minimumShelfLife.days());
|
||||
if (batch.expiryDate().isBefore(threshold)) {
|
||||
newStatus = StockBatchStatus.EXPIRING_SOON;
|
||||
}
|
||||
}
|
||||
|
||||
int index = this.batches.indexOf(batch);
|
||||
this.batches.set(index, batch.withStatus(newStatus));
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
// ==================== Getters ====================
|
||||
|
||||
public StockId id() { return id; }
|
||||
|
|
|
|||
|
|
@ -128,6 +128,10 @@ public class StockBatch {
|
|||
return reconstitute(this.id, this.batchReference, newQuantity, this.expiryDate, this.status, this.receivedAt);
|
||||
}
|
||||
|
||||
public StockBatch withStatus(StockBatchStatus newStatus) {
|
||||
return reconstitute(this.id, this.batchReference, this.quantity, this.expiryDate, newStatus, this.receivedAt);
|
||||
}
|
||||
|
||||
// ==================== Getters ====================
|
||||
|
||||
public StockBatchId id() { return id; }
|
||||
|
|
|
|||
|
|
@ -70,6 +70,16 @@ public sealed interface StockError {
|
|||
@Override public String message() { return "Batch " + id + " is not available for removal (status: " + status + ")"; }
|
||||
}
|
||||
|
||||
record BatchAlreadyBlocked(String id) implements StockError {
|
||||
@Override public String code() { return "BATCH_ALREADY_BLOCKED"; }
|
||||
@Override public String message() { return "Batch " + id + " is already blocked"; }
|
||||
}
|
||||
|
||||
record BatchNotBlocked(String id) implements StockError {
|
||||
@Override public String code() { return "BATCH_NOT_BLOCKED"; }
|
||||
@Override public String message() { return "Batch " + id + " is not blocked"; }
|
||||
}
|
||||
|
||||
record Unauthorized(String message) implements StockError {
|
||||
@Override public String code() { return "UNAUTHORIZED"; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package de.effigenix.infrastructure.config;
|
|||
|
||||
import de.effigenix.application.inventory.ActivateStorageLocation;
|
||||
import de.effigenix.application.inventory.AddStockBatch;
|
||||
import de.effigenix.application.inventory.BlockStockBatch;
|
||||
import de.effigenix.application.inventory.CreateStock;
|
||||
import de.effigenix.application.inventory.RemoveStockBatch;
|
||||
import de.effigenix.application.inventory.UnblockStockBatch;
|
||||
import de.effigenix.application.inventory.CreateStorageLocation;
|
||||
import de.effigenix.application.inventory.DeactivateStorageLocation;
|
||||
import de.effigenix.application.inventory.ListStorageLocations;
|
||||
|
|
@ -59,4 +61,14 @@ public class InventoryUseCaseConfiguration {
|
|||
public RemoveStockBatch removeStockBatch(StockRepository stockRepository) {
|
||||
return new RemoveStockBatch(stockRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BlockStockBatch blockStockBatch(StockRepository stockRepository) {
|
||||
return new BlockStockBatch(stockRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UnblockStockBatch unblockStockBatch(StockRepository stockRepository) {
|
||||
return new UnblockStockBatch(stockRepository);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
package de.effigenix.infrastructure.inventory.web.controller;
|
||||
|
||||
import de.effigenix.application.inventory.AddStockBatch;
|
||||
import de.effigenix.application.inventory.BlockStockBatch;
|
||||
import de.effigenix.application.inventory.CreateStock;
|
||||
import de.effigenix.application.inventory.RemoveStockBatch;
|
||||
import de.effigenix.application.inventory.UnblockStockBatch;
|
||||
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.UnblockStockBatchCommand;
|
||||
import de.effigenix.domain.inventory.StockError;
|
||||
import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest;
|
||||
import de.effigenix.infrastructure.inventory.web.dto.BlockStockBatchRequest;
|
||||
import de.effigenix.infrastructure.inventory.web.dto.CreateStockRequest;
|
||||
import de.effigenix.infrastructure.inventory.web.dto.RemoveStockBatchRequest;
|
||||
import de.effigenix.infrastructure.inventory.web.dto.StockBatchResponse;
|
||||
|
|
@ -34,11 +39,16 @@ public class StockController {
|
|||
private final CreateStock createStock;
|
||||
private final AddStockBatch addStockBatch;
|
||||
private final RemoveStockBatch removeStockBatch;
|
||||
private final BlockStockBatch blockStockBatch;
|
||||
private final UnblockStockBatch unblockStockBatch;
|
||||
|
||||
public StockController(CreateStock createStock, AddStockBatch addStockBatch, RemoveStockBatch removeStockBatch) {
|
||||
public StockController(CreateStock createStock, AddStockBatch addStockBatch, RemoveStockBatch removeStockBatch,
|
||||
BlockStockBatch blockStockBatch, UnblockStockBatch unblockStockBatch) {
|
||||
this.createStock = createStock;
|
||||
this.addStockBatch = addStockBatch;
|
||||
this.removeStockBatch = removeStockBatch;
|
||||
this.blockStockBatch = blockStockBatch;
|
||||
this.unblockStockBatch = unblockStockBatch;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
|
@ -115,6 +125,47 @@ public class StockController {
|
|||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{stockId}/batches/{batchId}/block")
|
||||
@PreAuthorize("hasAuthority('STOCK_WRITE')")
|
||||
public ResponseEntity<Void> blockBatch(
|
||||
@PathVariable String stockId,
|
||||
@PathVariable String batchId,
|
||||
@Valid @RequestBody BlockStockBatchRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
logger.info("Blocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName());
|
||||
|
||||
var cmd = new BlockStockBatchCommand(stockId, batchId, request.reason());
|
||||
var result = blockStockBatch.execute(cmd);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new StockDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Batch {} of stock {} blocked", batchId, stockId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{stockId}/batches/{batchId}/unblock")
|
||||
@PreAuthorize("hasAuthority('STOCK_WRITE')")
|
||||
public ResponseEntity<Void> unblockBatch(
|
||||
@PathVariable String stockId,
|
||||
@PathVariable String batchId,
|
||||
Authentication authentication
|
||||
) {
|
||||
logger.info("Unblocking batch {} of stock {} by actor: {}", batchId, stockId, authentication.getName());
|
||||
|
||||
var cmd = new UnblockStockBatchCommand(stockId, batchId);
|
||||
var result = unblockStockBatch.execute(cmd);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new StockDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Batch {} of stock {} unblocked", batchId, stockId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
public static class StockDomainErrorException extends RuntimeException {
|
||||
private final StockError error;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package de.effigenix.infrastructure.inventory.web.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record BlockStockBatchRequest(
|
||||
@NotBlank String reason
|
||||
) {}
|
||||
|
|
@ -30,6 +30,8 @@ public final class InventoryErrorHttpStatusMapper {
|
|||
case StockError.DuplicateBatchReference e -> 409;
|
||||
case StockError.NegativeStockNotAllowed e -> 409;
|
||||
case StockError.BatchNotAvailable e -> 409;
|
||||
case StockError.BatchAlreadyBlocked e -> 409;
|
||||
case StockError.BatchNotBlocked e -> 409;
|
||||
case StockError.InvalidMinimumLevel e -> 400;
|
||||
case StockError.InvalidMinimumShelfLife e -> 400;
|
||||
case StockError.InvalidArticleId e -> 400;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.application.inventory.command.BlockStockBatchCommand;
|
||||
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 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.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("BlockStockBatch Use Case")
|
||||
class BlockStockBatchTest {
|
||||
|
||||
@Mock private StockRepository stockRepository;
|
||||
|
||||
private BlockStockBatch blockStockBatch;
|
||||
private StockBatchId batchId;
|
||||
private Stock existingStock;
|
||||
private BlockStockBatchCommand validCommand;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
blockStockBatch = new BlockStockBatch(stockRepository);
|
||||
|
||||
batchId = StockBatchId.of("batch-1");
|
||||
var batch = StockBatch.reconstitute(
|
||||
batchId,
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null),
|
||||
LocalDate.of(2026, 12, 31),
|
||||
StockBatchStatus.AVAILABLE,
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
existingStock = 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))
|
||||
);
|
||||
|
||||
validCommand = new BlockStockBatchCommand("stock-1", "batch-1", "Quality issue");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should block batch successfully")
|
||||
void shouldBlockBatchSuccessfully() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(existingStock)));
|
||||
when(stockRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = blockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(stockRepository).save(existingStock);
|
||||
assertThat(existingStock.batches().getFirst().status()).isEqualTo(StockBatchStatus.BLOCKED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with StockNotFound when stock does not exist")
|
||||
void shouldFailWhenStockNotFound() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = blockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.StockNotFound.class);
|
||||
verify(stockRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = blockStockBatch.execute(validCommand);
|
||||
|
||||
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() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(existingStock)));
|
||||
when(stockRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = blockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should propagate domain error for batch not found")
|
||||
void shouldPropagateBatchNotFound() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(existingStock)));
|
||||
|
||||
var cmd = new BlockStockBatchCommand("stock-1", "nonexistent", "Quality issue");
|
||||
var result = blockStockBatch.execute(cmd);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchNotFound.class);
|
||||
verify(stockRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should propagate BatchAlreadyBlocked when batch is already blocked")
|
||||
void shouldPropagateBatchAlreadyBlocked() {
|
||||
var blockedBatch = StockBatch.reconstitute(
|
||||
batchId,
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null),
|
||||
LocalDate.of(2026, 12, 31),
|
||||
StockBatchStatus.BLOCKED,
|
||||
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(blockedBatch))
|
||||
);
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(stock)));
|
||||
|
||||
var result = blockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchAlreadyBlocked.class);
|
||||
verify(stockRepository, never()).save(any());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.application.inventory.command.UnblockStockBatchCommand;
|
||||
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 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.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("UnblockStockBatch Use Case")
|
||||
class UnblockStockBatchTest {
|
||||
|
||||
@Mock private StockRepository stockRepository;
|
||||
|
||||
private UnblockStockBatch unblockStockBatch;
|
||||
private StockBatchId batchId;
|
||||
private Stock existingStock;
|
||||
private UnblockStockBatchCommand validCommand;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
unblockStockBatch = new UnblockStockBatch(stockRepository);
|
||||
|
||||
batchId = StockBatchId.of("batch-1");
|
||||
var batch = StockBatch.reconstitute(
|
||||
batchId,
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null),
|
||||
LocalDate.of(2026, 12, 31),
|
||||
StockBatchStatus.BLOCKED,
|
||||
Instant.now()
|
||||
);
|
||||
|
||||
existingStock = 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))
|
||||
);
|
||||
|
||||
validCommand = new UnblockStockBatchCommand("stock-1", "batch-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should unblock batch successfully")
|
||||
void shouldUnblockBatchSuccessfully() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(existingStock)));
|
||||
when(stockRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = unblockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(stockRepository).save(existingStock);
|
||||
assertThat(existingStock.batches().getFirst().status()).isEqualTo(StockBatchStatus.AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with StockNotFound when stock does not exist")
|
||||
void shouldFailWhenStockNotFound() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = unblockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.StockNotFound.class);
|
||||
verify(stockRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = unblockStockBatch.execute(validCommand);
|
||||
|
||||
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() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(existingStock)));
|
||||
when(stockRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = unblockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should propagate domain error for batch not found")
|
||||
void shouldPropagateBatchNotFound() {
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(existingStock)));
|
||||
|
||||
var cmd = new UnblockStockBatchCommand("stock-1", "nonexistent");
|
||||
var result = unblockStockBatch.execute(cmd);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchNotFound.class);
|
||||
verify(stockRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should propagate BatchNotBlocked when batch is not blocked")
|
||||
void shouldPropagateBatchNotBlocked() {
|
||||
var availableBatch = StockBatch.reconstitute(
|
||||
batchId,
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null),
|
||||
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(availableBatch))
|
||||
);
|
||||
when(stockRepository.findById(StockId.of("stock-1")))
|
||||
.thenReturn(Result.success(Optional.of(stock)));
|
||||
|
||||
var result = unblockStockBatch.execute(validCommand);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchNotBlocked.class);
|
||||
verify(stockRepository, never()).save(any());
|
||||
}
|
||||
}
|
||||
|
|
@ -435,6 +435,166 @@ class StockTest {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== blockBatch ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("blockBatch()")
|
||||
class BlockBatch {
|
||||
|
||||
@Test
|
||||
@DisplayName("should block AVAILABLE batch")
|
||||
void shouldBlockAvailableBatch() {
|
||||
var stock = createStockWithBatch("10", UnitOfMeasure.KILOGRAM, StockBatchStatus.AVAILABLE);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.blockBatch(batchId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(stock.batches().getFirst().status()).isEqualTo(StockBatchStatus.BLOCKED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should block EXPIRING_SOON batch")
|
||||
void shouldBlockExpiringSoonBatch() {
|
||||
var stock = createStockWithBatch("10", UnitOfMeasure.KILOGRAM, StockBatchStatus.EXPIRING_SOON);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.blockBatch(batchId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(stock.batches().getFirst().status()).isEqualTo(StockBatchStatus.BLOCKED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when batch is already BLOCKED")
|
||||
void shouldFailWhenAlreadyBlocked() {
|
||||
var stock = createStockWithBatch("10", UnitOfMeasure.KILOGRAM, StockBatchStatus.BLOCKED);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.blockBatch(batchId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchAlreadyBlocked.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when batch is EXPIRED")
|
||||
void shouldFailWhenExpired() {
|
||||
var stock = createStockWithBatch("10", UnitOfMeasure.KILOGRAM, StockBatchStatus.EXPIRED);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.blockBatch(batchId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchNotAvailable.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when batch not found")
|
||||
void shouldFailWhenBatchNotFound() {
|
||||
var stock = createValidStock();
|
||||
|
||||
var result = stock.blockBatch(StockBatchId.of("nonexistent"));
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchNotFound.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== unblockBatch ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("unblockBatch()")
|
||||
class UnblockBatch {
|
||||
|
||||
@Test
|
||||
@DisplayName("should unblock to AVAILABLE when no minimum shelf life")
|
||||
void shouldUnblockToAvailable() {
|
||||
var stock = createStockWithBatch("10", UnitOfMeasure.KILOGRAM, StockBatchStatus.BLOCKED);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.unblockBatch(batchId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(stock.batches().getFirst().status()).isEqualTo(StockBatchStatus.AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should unblock to EXPIRING_SOON when MHD check triggers")
|
||||
void shouldUnblockToExpiringSoon() {
|
||||
var batch = StockBatch.reconstitute(
|
||||
StockBatchId.generate(),
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null),
|
||||
LocalDate.now().plusDays(5),
|
||||
StockBatchStatus.BLOCKED,
|
||||
Instant.now()
|
||||
);
|
||||
var stock = Stock.reconstitute(
|
||||
StockId.generate(),
|
||||
ArticleId.of("article-1"),
|
||||
StorageLocationId.of("location-1"),
|
||||
null, new MinimumShelfLife(30),
|
||||
new ArrayList<>(List.of(batch))
|
||||
);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.unblockBatch(batchId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(stock.batches().getFirst().status()).isEqualTo(StockBatchStatus.EXPIRING_SOON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should unblock to AVAILABLE when MHD check passes")
|
||||
void shouldUnblockToAvailableWhenMhdPasses() {
|
||||
var batch = StockBatch.reconstitute(
|
||||
StockBatchId.generate(),
|
||||
new BatchReference("BATCH-001", BatchType.PRODUCED),
|
||||
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null),
|
||||
LocalDate.now().plusDays(60),
|
||||
StockBatchStatus.BLOCKED,
|
||||
Instant.now()
|
||||
);
|
||||
var stock = Stock.reconstitute(
|
||||
StockId.generate(),
|
||||
ArticleId.of("article-1"),
|
||||
StorageLocationId.of("location-1"),
|
||||
null, new MinimumShelfLife(30),
|
||||
new ArrayList<>(List.of(batch))
|
||||
);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.unblockBatch(batchId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(stock.batches().getFirst().status()).isEqualTo(StockBatchStatus.AVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when batch is not BLOCKED")
|
||||
void shouldFailWhenNotBlocked() {
|
||||
var stock = createStockWithBatch("10", UnitOfMeasure.KILOGRAM, StockBatchStatus.AVAILABLE);
|
||||
var batchId = stock.batches().getFirst().id();
|
||||
|
||||
var result = stock.unblockBatch(batchId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchNotBlocked.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when batch not found")
|
||||
void shouldFailWhenBatchNotFound() {
|
||||
var stock = createValidStock();
|
||||
|
||||
var result = stock.unblockBatch(StockBatchId.of("nonexistent"));
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StockError.BatchNotFound.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private Stock createValidStock() {
|
||||
|
|
|
|||
|
|
@ -345,6 +345,209 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== Charge sperren (blockBatch) ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /{stockId}/batches/{batchId}/block – Charge sperren")
|
||||
class BlockBatch {
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge sperren → 200")
|
||||
void blockBatch_returns200() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId);
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Quality issue detected"}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Bereits gesperrte Charge erneut sperren → 409")
|
||||
void blockBatch_alreadyBlocked_returns409() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId);
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Quality issue detected"}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Another reason"}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_ALREADY_BLOCKED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge sperren – Batch nicht gefunden → 404")
|
||||
void blockBatch_batchNotFound_returns404() throws Exception {
|
||||
String stockId = createStock();
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, UUID.randomUUID().toString())
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Quality issue"}
|
||||
"""))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge sperren – Stock nicht gefunden → 404")
|
||||
void blockBatch_stockNotFound_returns404() throws Exception {
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block",
|
||||
UUID.randomUUID().toString(), UUID.randomUUID().toString())
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Quality issue"}
|
||||
"""))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge sperren ohne reason → 400")
|
||||
void blockBatch_withoutReason_returns400() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId);
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": ""}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge sperren ohne STOCK_WRITE → 403")
|
||||
void blockBatch_withViewerToken_returns403() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId);
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Quality issue"}
|
||||
"""))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge sperren ohne Token → 401")
|
||||
void blockBatch_withoutToken_returns401() throws Exception {
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block",
|
||||
UUID.randomUUID().toString(), UUID.randomUUID().toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Quality issue"}
|
||||
"""))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Charge entsperren (unblockBatch) ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("POST /{stockId}/batches/{batchId}/unblock – Charge entsperren")
|
||||
class UnblockBatch {
|
||||
|
||||
@Test
|
||||
@DisplayName("Gesperrte Charge entsperren → 200")
|
||||
void unblockBatch_returns200() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId);
|
||||
|
||||
// Erst sperren
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"reason": "Quality issue"}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// Dann entsperren
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nicht gesperrte Charge entsperren → 409")
|
||||
void unblockBatch_notBlocked_returns409() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId);
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_NOT_BLOCKED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge entsperren – Batch nicht gefunden → 404")
|
||||
void unblockBatch_batchNotFound_returns404() throws Exception {
|
||||
String stockId = createStock();
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, UUID.randomUUID().toString())
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("BATCH_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge entsperren – Stock nicht gefunden → 404")
|
||||
void unblockBatch_stockNotFound_returns404() throws Exception {
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock",
|
||||
UUID.randomUUID().toString(), UUID.randomUUID().toString())
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge entsperren ohne STOCK_WRITE → 403")
|
||||
void unblockBatch_withViewerToken_returns403() throws Exception {
|
||||
String stockId = createStock();
|
||||
String batchId = addBatchToStock(stockId);
|
||||
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, batchId)
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Charge entsperren ohne Token → 401")
|
||||
void unblockBatch_withoutToken_returns401() throws Exception {
|
||||
mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock",
|
||||
UUID.randomUUID().toString(), UUID.randomUUID().toString())
|
||||
.contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Hilfsmethoden ====================
|
||||
|
||||
private String createStorageLocation() throws Exception {
|
||||
|
|
@ -362,6 +565,21 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
|
|||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String addBatchToStock(String stockId) throws Exception {
|
||||
var request = new AddStockBatchRequest(
|
||||
"BATCH-" + UUID.randomUUID().toString().substring(0, 8),
|
||||
"PRODUCED", "10", "KILOGRAM", "2026-12-31");
|
||||
|
||||
var result = mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
|
||||
.header("Authorization", "Bearer " + adminToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
|
||||
}
|
||||
|
||||
private String createStock() throws Exception {
|
||||
var request = new CreateStockRequest(
|
||||
UUID.randomUUID().toString(), storageLocationId, null, null, null);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue