diff --git a/backend/src/main/java/de/effigenix/application/production/StartProductionOrder.java b/backend/src/main/java/de/effigenix/application/production/StartProductionOrder.java index 4d1707f..b97e31b 100644 --- a/backend/src/main/java/de/effigenix/application/production/StartProductionOrder.java +++ b/backend/src/main/java/de/effigenix/application/production/StartProductionOrder.java @@ -11,22 +11,28 @@ public class StartProductionOrder { private final ProductionOrderRepository productionOrderRepository; private final BatchRepository batchRepository; + private final RecipeRepository recipeRepository; + private final BatchNumberGenerator batchNumberGenerator; private final AuthorizationPort authorizationPort; private final UnitOfWork unitOfWork; public StartProductionOrder( ProductionOrderRepository productionOrderRepository, BatchRepository batchRepository, + RecipeRepository recipeRepository, + BatchNumberGenerator batchNumberGenerator, AuthorizationPort authorizationPort, UnitOfWork unitOfWork ) { this.productionOrderRepository = productionOrderRepository; this.batchRepository = batchRepository; + this.recipeRepository = recipeRepository; + this.batchNumberGenerator = batchNumberGenerator; this.authorizationPort = authorizationPort; this.unitOfWork = unitOfWork; } - public Result execute(StartProductionOrderCommand cmd, ActorId performedBy) { + public Result execute(StartProductionOrderCommand cmd, ActorId performedBy) { if (!authorizationPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)) { return Result.failure(new ProductionOrderError.Unauthorized("Not authorized to start production orders")); } @@ -46,56 +52,80 @@ public class StartProductionOrder { } } - // Load batch - var batchId = BatchId.of(cmd.batchId()); - Batch batch; - switch (batchRepository.findById(batchId)) { + // Load recipe + Recipe recipe; + switch (recipeRepository.findById(order.recipeId())) { case Result.Failure(var err) -> { return Result.failure(new ProductionOrderError.RepositoryFailure(err.message())); } case Result.Success(var opt) -> { if (opt.isEmpty()) { - return Result.failure(new ProductionOrderError.ValidationFailure("Batch '" + cmd.batchId() + "' not found")); + return Result.failure(new ProductionOrderError.ValidationFailure( + "Recipe '" + order.recipeId().value() + "' not found")); } - batch = opt.get(); + recipe = opt.get(); } } - // Batch must be PLANNED - if (batch.status() != BatchStatus.PLANNED) { + // Recipe must be ACTIVE + if (recipe.status() != RecipeStatus.ACTIVE) { return Result.failure(new ProductionOrderError.ValidationFailure( - "Batch '" + cmd.batchId() + "' is not in PLANNED status (current: " + batch.status() + ")")); + "Recipe '" + recipe.id().value() + "' is not ACTIVE (current: " + recipe.status() + ")")); } - // Batch must reference the same recipe as the order - if (!batch.recipeId().equals(order.recipeId())) { - return Result.failure(new ProductionOrderError.ValidationFailure( - "Batch recipe '" + batch.recipeId().value() + "' does not match order recipe '" + order.recipeId().value() + "'")); + // Calculate bestBeforeDate (validates shelfLifeDays internally) + java.time.LocalDate bestBeforeDate; + switch (recipe.calculateBestBeforeDate(order.plannedDate())) { + case Result.Failure(var err) -> { + return Result.failure(new ProductionOrderError.ValidationFailure( + "Recipe '" + recipe.id().value() + "' has no valid shelf life configured: " + err.message())); + } + case Result.Success(var val) -> bestBeforeDate = val; } - // Start production on order (RELEASED -> IN_PROGRESS, assigns batchId) - switch (order.startProduction(batchId)) { - case Result.Failure(var err) -> { return Result.failure(err); } - case Result.Success(var ignored) -> { } + // Generate batch number + BatchNumber batchNumber; + switch (batchNumberGenerator.generateNext(order.plannedDate())) { + case Result.Failure(var err) -> { + return Result.failure(new ProductionOrderError.RepositoryFailure(err.message())); + } + case Result.Success(var val) -> batchNumber = val; + } + + // Build BatchDraft from order data + var batchDraft = new BatchDraft( + order.recipeId().value(), + order.plannedQuantity().amount().toPlainString(), + order.plannedQuantity().uom().name(), + order.plannedDate(), + bestBeforeDate + ); + + // Create batch in PLANNED status + Batch batch; + switch (Batch.plan(batchDraft, batchNumber)) { + case Result.Failure(var err) -> { + return Result.failure(new ProductionOrderError.BatchCreationFailed(err.code(), err.message())); + } + case Result.Success(var val) -> batch = val; } // Start production on batch (PLANNED -> IN_PRODUCTION) switch (batch.startProduction()) { case Result.Failure(var err) -> { - return Result.failure(new ProductionOrderError.ValidationFailure(err.message())); + return Result.failure(new ProductionOrderError.BatchCreationFailed(err.code(), err.message())); } case Result.Success(var ignored) -> { } } - // Persist both atomically - return unitOfWork.executeAtomically(() -> { - switch (productionOrderRepository.save(order)) { - case Result.Failure(var err) -> { - return Result.failure(new ProductionOrderError.RepositoryFailure(err.message())); - } - case Result.Success(var ignored) -> { } - } + // Start production on order (RELEASED -> IN_PROGRESS, assigns batchId) + switch (order.startProduction(batch.id())) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var ignored) -> { } + } + // Persist both atomically (batch first due to FK constraint on production_orders.batch_id) + return unitOfWork.executeAtomically(() -> { switch (batchRepository.save(batch)) { case Result.Failure(var err) -> { return Result.failure(new ProductionOrderError.RepositoryFailure(err.message())); @@ -103,7 +133,14 @@ public class StartProductionOrder { case Result.Success(var ignored) -> { } } - return Result.success(order); + switch (productionOrderRepository.save(order)) { + case Result.Failure(var err) -> { + return Result.failure(new ProductionOrderError.RepositoryFailure(err.message())); + } + case Result.Success(var ignored) -> { } + } + + return Result.success(new StartProductionResult(order, batch)); }); } } diff --git a/backend/src/main/java/de/effigenix/application/production/StartProductionResult.java b/backend/src/main/java/de/effigenix/application/production/StartProductionResult.java new file mode 100644 index 0000000..e93fa20 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/production/StartProductionResult.java @@ -0,0 +1,7 @@ +package de.effigenix.application.production; + +import de.effigenix.domain.production.Batch; +import de.effigenix.domain.production.ProductionOrder; + +public record StartProductionResult(ProductionOrder order, Batch batch) { +} diff --git a/backend/src/main/java/de/effigenix/application/production/command/StartProductionOrderCommand.java b/backend/src/main/java/de/effigenix/application/production/command/StartProductionOrderCommand.java index 6fd3e52..e78e73a 100644 --- a/backend/src/main/java/de/effigenix/application/production/command/StartProductionOrderCommand.java +++ b/backend/src/main/java/de/effigenix/application/production/command/StartProductionOrderCommand.java @@ -1,4 +1,4 @@ package de.effigenix.application.production.command; -public record StartProductionOrderCommand(String productionOrderId, String batchId) { +public record StartProductionOrderCommand(String productionOrderId) { } diff --git a/backend/src/main/java/de/effigenix/domain/production/ProductionOrderError.java b/backend/src/main/java/de/effigenix/domain/production/ProductionOrderError.java index bb8e19a..d3bab7d 100644 --- a/backend/src/main/java/de/effigenix/domain/production/ProductionOrderError.java +++ b/backend/src/main/java/de/effigenix/domain/production/ProductionOrderError.java @@ -55,6 +55,10 @@ public sealed interface ProductionOrderError { @Override public String message() { return "Cannot reschedule production order in status " + current; } } + record BatchCreationFailed(String batchErrorCode, String message) implements ProductionOrderError { + @Override public String code() { return "PRODUCTION_ORDER_BATCH_CREATION_FAILED"; } + } + record ValidationFailure(String message) implements ProductionOrderError { @Override public String code() { return "PRODUCTION_ORDER_VALIDATION_ERROR"; } } diff --git a/backend/src/main/java/de/effigenix/domain/production/Recipe.java b/backend/src/main/java/de/effigenix/domain/production/Recipe.java index cec635d..c0367f6 100644 --- a/backend/src/main/java/de/effigenix/domain/production/Recipe.java +++ b/backend/src/main/java/de/effigenix/domain/production/Recipe.java @@ -253,6 +253,16 @@ public class Recipe { return Result.success(null); } + // ==================== Business Logic ==================== + + public Result calculateBestBeforeDate(java.time.LocalDate productionDate) { + if (shelfLifeDays == null || shelfLifeDays <= 0) { + return Result.failure(new RecipeError.InvalidShelfLife( + "ShelfLifeDays must be > 0, was: " + shelfLifeDays)); + } + return Result.success(productionDate.plusDays(shelfLifeDays)); + } + // ==================== Getters ==================== public RecipeId id() { return id; } diff --git a/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java b/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java index a9acc8c..adcee4b 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java +++ b/backend/src/main/java/de/effigenix/infrastructure/config/ProductionUseCaseConfiguration.java @@ -161,9 +161,12 @@ public class ProductionUseCaseConfiguration { @Bean public StartProductionOrder startProductionOrder(ProductionOrderRepository productionOrderRepository, BatchRepository batchRepository, + RecipeRepository recipeRepository, + BatchNumberGenerator batchNumberGenerator, AuthorizationPort authorizationPort, UnitOfWork unitOfWork) { - return new StartProductionOrder(productionOrderRepository, batchRepository, authorizationPort, unitOfWork); + return new StartProductionOrder(productionOrderRepository, batchRepository, recipeRepository, + batchNumberGenerator, authorizationPort, unitOfWork); } @Bean diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java index 9159f7d..94ffaa5 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/controller/ProductionOrderController.java @@ -14,13 +14,15 @@ import de.effigenix.application.production.command.CreateProductionOrderCommand; import de.effigenix.application.production.command.ReleaseProductionOrderCommand; import de.effigenix.application.production.command.RescheduleProductionOrderCommand; import de.effigenix.application.production.command.StartProductionOrderCommand; +import de.effigenix.domain.production.BatchRepository; +import de.effigenix.domain.production.ProductionOrder; import de.effigenix.domain.production.ProductionOrderError; import de.effigenix.domain.production.ProductionOrderStatus; import de.effigenix.infrastructure.production.web.dto.CancelProductionOrderRequest; import de.effigenix.infrastructure.production.web.dto.CreateProductionOrderRequest; import de.effigenix.infrastructure.production.web.dto.ProductionOrderResponse; import de.effigenix.infrastructure.production.web.dto.RescheduleProductionOrderRequest; -import de.effigenix.infrastructure.production.web.dto.StartProductionOrderRequest; + import de.effigenix.shared.security.ActorId; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -53,6 +55,7 @@ public class ProductionOrderController { private final CompleteProductionOrder completeProductionOrder; private final CancelProductionOrder cancelProductionOrder; private final ListProductionOrders listProductionOrders; + private final BatchRepository batchRepository; public ProductionOrderController(CreateProductionOrder createProductionOrder, GetProductionOrder getProductionOrder, @@ -61,7 +64,8 @@ public class ProductionOrderController { StartProductionOrder startProductionOrder, CompleteProductionOrder completeProductionOrder, CancelProductionOrder cancelProductionOrder, - ListProductionOrders listProductionOrders) { + ListProductionOrders listProductionOrders, + BatchRepository batchRepository) { this.createProductionOrder = createProductionOrder; this.getProductionOrder = getProductionOrder; this.releaseProductionOrder = releaseProductionOrder; @@ -70,6 +74,7 @@ public class ProductionOrderController { this.completeProductionOrder = completeProductionOrder; this.cancelProductionOrder = cancelProductionOrder; this.listProductionOrders = listProductionOrders; + this.batchRepository = batchRepository; } @GetMapping @@ -104,7 +109,7 @@ public class ProductionOrderController { } var responses = result.unsafeGetValue().stream() - .map(ProductionOrderResponse::from) + .map(order -> ProductionOrderResponse.from(order, resolveBatchNumber(order))) .toList(); return ResponseEntity.ok(responses); } @@ -120,7 +125,7 @@ public class ProductionOrderController { throw new ProductionOrderDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue())); + return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue(), resolveBatchNumber(result.unsafeGetValue()))); } @PostMapping @@ -147,7 +152,7 @@ public class ProductionOrderController { } return ResponseEntity.status(HttpStatus.CREATED) - .body(ProductionOrderResponse.from(result.unsafeGetValue())); + .body(ProductionOrderResponse.from(result.unsafeGetValue(), resolveBatchNumber(result.unsafeGetValue()))); } @PostMapping("/{id}/reschedule") @@ -166,7 +171,7 @@ public class ProductionOrderController { throw new ProductionOrderDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue())); + return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue(), resolveBatchNumber(result.unsafeGetValue()))); } @PostMapping("/{id}/release") @@ -184,26 +189,36 @@ public class ProductionOrderController { throw new ProductionOrderDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue())); + return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue(), resolveBatchNumber(result.unsafeGetValue()))); } @PostMapping("/{id}/start") @PreAuthorize("hasAuthority('PRODUCTION_ORDER_WRITE')") public ResponseEntity startProductionOrder( @PathVariable String id, - @Valid @RequestBody StartProductionOrderRequest request, Authentication authentication ) { - logger.info("Starting production for order: {} with batch: {} by actor: {}", id, request.batchId(), authentication.getName()); + logger.info("Starting production for order: {} by actor: {}", id, authentication.getName()); - var cmd = new StartProductionOrderCommand(id, request.batchId()); + var cmd = new StartProductionOrderCommand(id); var result = startProductionOrder.execute(cmd, ActorId.of(authentication.getName())); if (result.isFailure()) { throw new ProductionOrderDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue())); + var startResult = result.unsafeGetValue(); + return ResponseEntity.ok(ProductionOrderResponse.from( + startResult.order(), + startResult.batch().batchNumber().value())); + } + + private String resolveBatchNumber(ProductionOrder order) { + if (order.batchId() == null) { + return null; + } + return batchRepository.findById(order.batchId()) + .fold(err -> null, opt -> opt.map(batch -> batch.batchNumber().value()).orElse(null)); } @PostMapping("/{id}/complete") @@ -221,7 +236,7 @@ public class ProductionOrderController { throw new ProductionOrderDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue())); + return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue(), resolveBatchNumber(result.unsafeGetValue()))); } @PostMapping("/{id}/cancel") @@ -240,7 +255,7 @@ public class ProductionOrderController { throw new ProductionOrderDomainErrorException(result.unsafeGetError()); } - return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue())); + return ResponseEntity.ok(ProductionOrderResponse.from(result.unsafeGetValue(), resolveBatchNumber(result.unsafeGetValue()))); } public static class ProductionOrderDomainErrorException extends RuntimeException { diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/ProductionOrderResponse.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/ProductionOrderResponse.java index 42297a8..5aef3a6 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/ProductionOrderResponse.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/ProductionOrderResponse.java @@ -10,6 +10,7 @@ public record ProductionOrderResponse( String recipeId, String status, String batchId, + String batchNumber, String plannedQuantity, String plannedQuantityUnit, LocalDate plannedDate, @@ -19,12 +20,13 @@ public record ProductionOrderResponse( OffsetDateTime createdAt, OffsetDateTime updatedAt ) { - public static ProductionOrderResponse from(ProductionOrder order) { + public static ProductionOrderResponse from(ProductionOrder order, String batchNumber) { return new ProductionOrderResponse( order.id().value(), order.recipeId().value(), order.status().name(), order.batchId() != null ? order.batchId().value() : null, + batchNumber, order.plannedQuantity().amount().toPlainString(), order.plannedQuantity().uom().name(), order.plannedDate(), diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/StartProductionOrderRequest.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/StartProductionOrderRequest.java deleted file mode 100644 index 5b4b9eb..0000000 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/dto/StartProductionOrderRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package de.effigenix.infrastructure.production.web.dto; - -import jakarta.validation.constraints.NotBlank; - -public record StartProductionOrderRequest(@NotBlank String batchId) { -} diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/web/exception/ProductionErrorHttpStatusMapper.java b/backend/src/main/java/de/effigenix/infrastructure/production/web/exception/ProductionErrorHttpStatusMapper.java index a81bfd3..a26c217 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/web/exception/ProductionErrorHttpStatusMapper.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/web/exception/ProductionErrorHttpStatusMapper.java @@ -61,6 +61,7 @@ public final class ProductionErrorHttpStatusMapper { case ProductionOrderError.RescheduleNotAllowed e -> 409; case ProductionOrderError.BatchAlreadyAssigned e -> 409; case ProductionOrderError.BatchNotCompleted e -> 409; + case ProductionOrderError.BatchCreationFailed e -> 400; case ProductionOrderError.ValidationFailure e -> 400; case ProductionOrderError.Unauthorized e -> 403; case ProductionOrderError.RepositoryFailure e -> 500; diff --git a/backend/src/test/java/de/effigenix/application/production/StartProductionOrderTest.java b/backend/src/test/java/de/effigenix/application/production/StartProductionOrderTest.java index ebb6935..98d4699 100644 --- a/backend/src/test/java/de/effigenix/application/production/StartProductionOrderTest.java +++ b/backend/src/test/java/de/effigenix/application/production/StartProductionOrderTest.java @@ -34,6 +34,8 @@ class StartProductionOrderTest { @Mock private ProductionOrderRepository productionOrderRepository; @Mock private BatchRepository batchRepository; + @Mock private RecipeRepository recipeRepository; + @Mock private BatchNumberGenerator batchNumberGenerator; @Mock private AuthorizationPort authPort; @Mock private UnitOfWork unitOfWork; @@ -41,16 +43,19 @@ class StartProductionOrderTest { private ActorId performedBy; private static final LocalDate PLANNED_DATE = LocalDate.now().plusDays(7); + private static final int SHELF_LIFE_DAYS = 14; @BeforeEach void setUp() { - startProductionOrder = new StartProductionOrder(productionOrderRepository, batchRepository, authPort, unitOfWork); + startProductionOrder = new StartProductionOrder( + productionOrderRepository, batchRepository, recipeRepository, + batchNumberGenerator, authPort, unitOfWork); performedBy = ActorId.of("admin-user"); lenient().when(unitOfWork.executeAtomically(any())).thenAnswer(inv -> ((Supplier) inv.getArgument(0)).get()); } private StartProductionOrderCommand validCommand() { - return new StartProductionOrderCommand("order-1", "batch-1"); + return new StartProductionOrderCommand("order-1"); } private ProductionOrder releasedOrder() { @@ -87,78 +92,90 @@ class StartProductionOrderTest { ); } - private Batch plannedBatch() { - return Batch.reconstitute( - BatchId.of("batch-1"), - new BatchNumber("P-2026-02-24-001"), + private Recipe activeRecipe() { + return Recipe.reconstitute( RecipeId.of("recipe-1"), - BatchStatus.PLANNED, + RecipeName.of("Test Recipe").unsafeGetValue(), + 1, + RecipeType.FINISHED_PRODUCT, + "Test", + YieldPercentage.of(100).unsafeGetValue(), + SHELF_LIFE_DAYS, Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), - null, null, null, - PLANNED_DATE, PLANNED_DATE.plusDays(14), + "ART-001", + RecipeStatus.ACTIVE, + List.of(), + List.of(), OffsetDateTime.now(ZoneOffset.UTC), - OffsetDateTime.now(ZoneOffset.UTC), - null, null, null, - 1L, - List.of() + OffsetDateTime.now(ZoneOffset.UTC) ); } - private Batch plannedBatchWithDifferentRecipe() { - return Batch.reconstitute( - BatchId.of("batch-1"), - new BatchNumber("P-2026-02-24-001"), - RecipeId.of("recipe-other"), - BatchStatus.PLANNED, - Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), - null, null, null, - PLANNED_DATE, PLANNED_DATE.plusDays(14), - OffsetDateTime.now(ZoneOffset.UTC), - OffsetDateTime.now(ZoneOffset.UTC), - null, null, null, - 1L, - List.of() - ); - } - - private Batch inProductionBatch() { - return Batch.reconstitute( - BatchId.of("batch-1"), - new BatchNumber("P-2026-02-24-001"), + private Recipe draftRecipe() { + return Recipe.reconstitute( RecipeId.of("recipe-1"), - BatchStatus.IN_PRODUCTION, + RecipeName.of("Test Recipe").unsafeGetValue(), + 1, + RecipeType.FINISHED_PRODUCT, + "Test", + YieldPercentage.of(100).unsafeGetValue(), + SHELF_LIFE_DAYS, Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), - null, null, null, - PLANNED_DATE, PLANNED_DATE.plusDays(14), + "ART-001", + RecipeStatus.DRAFT, + List.of(), + List.of(), OffsetDateTime.now(ZoneOffset.UTC), - OffsetDateTime.now(ZoneOffset.UTC), - null, null, null, - 1L, - List.of() + OffsetDateTime.now(ZoneOffset.UTC) ); } - @Test - @DisplayName("should start production when order is RELEASED and batch is PLANNED") - void should_StartProduction_When_ValidCommand() { + private BatchNumber batchNumber() { + return new BatchNumber("P-" + PLANNED_DATE + "-001"); + } + + private void setupHappyPath() { when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(plannedBatch()))); + when(recipeRepository.findById(RecipeId.of("recipe-1"))) + .thenReturn(Result.success(Optional.of(activeRecipe()))); + when(batchNumberGenerator.generateNext(PLANNED_DATE)) + .thenReturn(Result.success(batchNumber())); when(productionOrderRepository.save(any())).thenReturn(Result.success(null)); when(batchRepository.save(any())).thenReturn(Result.success(null)); + } + + @Test + @DisplayName("should auto-create batch and start production when order is RELEASED") + void should_StartProduction_When_ValidCommand() { + setupHappyPath(); var result = startProductionOrder.execute(validCommand(), performedBy); assertThat(result.isSuccess()).isTrue(); - var order = result.unsafeGetValue(); - assertThat(order.status()).isEqualTo(ProductionOrderStatus.IN_PROGRESS); - assertThat(order.batchId()).isEqualTo(BatchId.of("batch-1")); + var startResult = result.unsafeGetValue(); + assertThat(startResult.order().status()).isEqualTo(ProductionOrderStatus.IN_PROGRESS); + assertThat(startResult.order().batchId()).isNotNull(); + assertThat(startResult.batch().status()).isEqualTo(BatchStatus.IN_PRODUCTION); + assertThat(startResult.batch().batchNumber()).isEqualTo(batchNumber()); + assertThat(startResult.batch().recipeId()).isEqualTo(RecipeId.of("recipe-1")); verify(productionOrderRepository).save(any(ProductionOrder.class)); verify(batchRepository).save(any(Batch.class)); } + @Test + @DisplayName("should calculate bestBeforeDate from plannedDate + shelfLifeDays") + void should_CalculateBestBeforeDate() { + setupHappyPath(); + + var result = startProductionOrder.execute(validCommand(), performedBy); + + assertThat(result.isSuccess()).isTrue(); + var batch = result.unsafeGetValue().batch(); + assertThat(batch.bestBeforeDate()).isEqualTo(PLANNED_DATE.plusDays(SHELF_LIFE_DAYS)); + } + @Test @DisplayName("should fail when actor lacks PRODUCTION_ORDER_WRITE permission") void should_Fail_When_Unauthorized() { @@ -186,52 +203,36 @@ class StartProductionOrderTest { } @Test - @DisplayName("should fail when batch not found") - void should_Fail_When_BatchNotFound() { + @DisplayName("should fail when recipe not found") + void should_Fail_When_RecipeNotFound() { when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) + when(recipeRepository.findById(RecipeId.of("recipe-1"))) .thenReturn(Result.success(Optional.empty())); var result = startProductionOrder.execute(validCommand(), performedBy); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class); - verify(productionOrderRepository, never()).save(any()); + assertThat(result.unsafeGetError().message()).contains("Recipe"); + assertThat(result.unsafeGetError().message()).contains("not found"); } @Test - @DisplayName("should fail when batch is not PLANNED") - void should_Fail_When_BatchNotPlanned() { + @DisplayName("should fail when recipe is not ACTIVE") + void should_Fail_When_RecipeNotActive() { when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(inProductionBatch()))); + when(recipeRepository.findById(RecipeId.of("recipe-1"))) + .thenReturn(Result.success(Optional.of(draftRecipe()))); var result = startProductionOrder.execute(validCommand(), performedBy); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class); - verify(productionOrderRepository, never()).save(any()); - } - - @Test - @DisplayName("should fail when batch recipe does not match order recipe") - void should_Fail_When_RecipeMismatch() { - when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); - when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) - .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(plannedBatchWithDifferentRecipe()))); - - var result = startProductionOrder.execute(validCommand(), performedBy); - - assertThat(result.isFailure()).isTrue(); - assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class); - assertThat(result.unsafeGetError().message()).contains("does not match order recipe"); - verify(productionOrderRepository, never()).save(any()); + assertThat(result.unsafeGetError().message()).contains("not ACTIVE"); } @Test @@ -240,8 +241,10 @@ class StartProductionOrderTest { when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(plannedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(plannedBatch()))); + when(recipeRepository.findById(RecipeId.of("recipe-1"))) + .thenReturn(Result.success(Optional.of(activeRecipe()))); + when(batchNumberGenerator.generateNext(PLANNED_DATE)) + .thenReturn(Result.success(batchNumber())); var result = startProductionOrder.execute(validCommand(), performedBy); @@ -265,12 +268,12 @@ class StartProductionOrderTest { } @Test - @DisplayName("should fail when batch repository findById returns error") - void should_Fail_When_BatchRepositoryError() { + @DisplayName("should fail when recipe repository returns error") + void should_Fail_When_RecipeRepositoryError() { when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) + when(recipeRepository.findById(RecipeId.of("recipe-1"))) .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); var result = startProductionOrder.execute(validCommand(), performedBy); @@ -280,32 +283,16 @@ class StartProductionOrderTest { verify(productionOrderRepository, never()).save(any()); } - @Test - @DisplayName("should fail when order save fails") - void should_Fail_When_OrderSaveFails() { - when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); - when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) - .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(plannedBatch()))); - when(productionOrderRepository.save(any())) - .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full"))); - - var result = startProductionOrder.execute(validCommand(), performedBy); - - assertThat(result.isFailure()).isTrue(); - assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class); - } - @Test @DisplayName("should fail when batch save fails") void should_Fail_When_BatchSaveFails() { when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(plannedBatch()))); - when(productionOrderRepository.save(any())).thenReturn(Result.success(null)); + when(recipeRepository.findById(RecipeId.of("recipe-1"))) + .thenReturn(Result.success(Optional.of(activeRecipe()))); + when(batchNumberGenerator.generateNext(PLANNED_DATE)) + .thenReturn(Result.success(batchNumber())); when(batchRepository.save(any())) .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full"))); @@ -316,68 +303,126 @@ class StartProductionOrderTest { } @Test - @DisplayName("should fail when batch is COMPLETED") - void should_Fail_When_BatchCompleted() { - var completedBatch = Batch.reconstitute( - BatchId.of("batch-1"), - new BatchNumber("P-2026-02-24-001"), + @DisplayName("should fail when order save fails") + void should_Fail_When_OrderSaveFails() { + setupHappyPath(); + when(productionOrderRepository.save(any())) + .thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full"))); + + var result = startProductionOrder.execute(validCommand(), performedBy); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class); + } + + @Test + @DisplayName("should fail when batch number generation fails") + void should_Fail_When_BatchNumberGenerationFails() { + when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); + when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) + .thenReturn(Result.success(Optional.of(releasedOrder()))); + when(recipeRepository.findById(RecipeId.of("recipe-1"))) + .thenReturn(Result.success(Optional.of(activeRecipe()))); + when(batchNumberGenerator.generateNext(PLANNED_DATE)) + .thenReturn(Result.failure(new BatchError.RepositoryFailure("Sequence corrupted"))); + + var result = startProductionOrder.execute(validCommand(), performedBy); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class); + verify(productionOrderRepository, never()).save(any()); + verify(batchRepository, never()).save(any()); + } + + @Test + @DisplayName("should fail when recipe has null shelfLifeDays") + void should_Fail_When_ShelfLifeDaysNull() { + var recipeWithoutShelfLife = Recipe.reconstitute( RecipeId.of("recipe-1"), - BatchStatus.COMPLETED, + RecipeName.of("Test Recipe").unsafeGetValue(), + 1, + RecipeType.FINISHED_PRODUCT, + "Test", + YieldPercentage.of(100).unsafeGetValue(), + null, Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), - Quantity.of(new BigDecimal("95"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), - Quantity.of(new BigDecimal("5"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), - "done", - PLANNED_DATE, PLANNED_DATE.plusDays(14), + "ART-001", + RecipeStatus.ACTIVE, + List.of(), + List.of(), OffsetDateTime.now(ZoneOffset.UTC), - OffsetDateTime.now(ZoneOffset.UTC), - OffsetDateTime.now(ZoneOffset.UTC), - null, null, - 1L, - List.of() + OffsetDateTime.now(ZoneOffset.UTC) ); when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(completedBatch))); + when(recipeRepository.findById(RecipeId.of("recipe-1"))) + .thenReturn(Result.success(Optional.of(recipeWithoutShelfLife))); var result = startProductionOrder.execute(validCommand(), performedBy); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class); + assertThat(result.unsafeGetError().message()).contains("shelf life"); verify(productionOrderRepository, never()).save(any()); } @Test - @DisplayName("should fail when batch is CANCELLED") - void should_Fail_When_BatchCancelled() { - var cancelledBatch = Batch.reconstitute( - BatchId.of("batch-1"), - new BatchNumber("P-2026-02-24-001"), + @DisplayName("should fail when recipe has zero shelfLifeDays") + void should_Fail_When_ShelfLifeDaysZero() { + var recipeWithZeroShelfLife = Recipe.reconstitute( RecipeId.of("recipe-1"), - BatchStatus.CANCELLED, + RecipeName.of("Test Recipe").unsafeGetValue(), + 1, + RecipeType.FINISHED_PRODUCT, + "Test", + YieldPercentage.of(100).unsafeGetValue(), + 0, Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), - null, null, null, - PLANNED_DATE, PLANNED_DATE.plusDays(14), + "ART-001", + RecipeStatus.ACTIVE, + List.of(), + List.of(), OffsetDateTime.now(ZoneOffset.UTC), - OffsetDateTime.now(ZoneOffset.UTC), - null, - "Storniert", OffsetDateTime.now(ZoneOffset.UTC), - 1L, - List.of() + OffsetDateTime.now(ZoneOffset.UTC) ); when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_WRITE)).thenReturn(true); when(productionOrderRepository.findById(ProductionOrderId.of("order-1"))) .thenReturn(Result.success(Optional.of(releasedOrder()))); - when(batchRepository.findById(BatchId.of("batch-1"))) - .thenReturn(Result.success(Optional.of(cancelledBatch))); + when(recipeRepository.findById(RecipeId.of("recipe-1"))) + .thenReturn(Result.success(Optional.of(recipeWithZeroShelfLife))); var result = startProductionOrder.execute(validCommand(), performedBy); assertThat(result.isFailure()).isTrue(); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.ValidationFailure.class); - verify(productionOrderRepository, never()).save(any()); + assertThat(result.unsafeGetError().message()).contains("shelf life"); + } + + @Test + @DisplayName("should set batch plannedQuantity and UOM from order") + void should_PropagateQuantityFromOrderToBatch() { + setupHappyPath(); + + var result = startProductionOrder.execute(validCommand(), performedBy); + + assertThat(result.isSuccess()).isTrue(); + var batch = result.unsafeGetValue().batch(); + assertThat(batch.plannedQuantity().amount()).isEqualByComparingTo(new BigDecimal("100")); + assertThat(batch.plannedQuantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM); + } + + @Test + @DisplayName("should assign generated batch id to the order") + void should_AssignBatchIdToOrder() { + setupHappyPath(); + + var result = startProductionOrder.execute(validCommand(), performedBy); + + assertThat(result.isSuccess()).isTrue(); + var startResult = result.unsafeGetValue(); + assertThat(startResult.order().batchId()).isEqualTo(startResult.batch().id()); } } diff --git a/backend/src/test/java/de/effigenix/domain/production/ProductionOrderFuzzTest.java b/backend/src/test/java/de/effigenix/domain/production/ProductionOrderFuzzTest.java index 271bc81..55993a2 100644 --- a/backend/src/test/java/de/effigenix/domain/production/ProductionOrderFuzzTest.java +++ b/backend/src/test/java/de/effigenix/domain/production/ProductionOrderFuzzTest.java @@ -39,11 +39,7 @@ class ProductionOrderFuzzTest { int op = data.consumeInt(0, 4); switch (op) { case 0 -> order.release(); - case 1 -> { - try { - order.startProduction(BatchId.of(data.consumeString(50))); - } catch (Exception ignored) { } - } + case 1 -> order.startProduction(BatchId.of(data.consumeString(50))); case 2 -> order.complete(); case 3 -> order.cancel(data.consumeString(50)); case 4 -> order.reschedule(consumeLocalDate(data)); diff --git a/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java index 72f94ff..ac129f6 100644 --- a/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java +++ b/backend/src/test/java/de/effigenix/infrastructure/production/web/ProductionOrderControllerIntegrationTest.java @@ -389,41 +389,26 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { class StartProductionOrderEndpoint { @Test - @DisplayName("RELEASED Order mit PLANNED Batch starten → 200, Status IN_PROGRESS") - void startOrder_releasedWithPlannedBatch_returns200() throws Exception { - String[] orderAndRecipe = createReleasedOrderWithRecipe(); - String orderId = orderAndRecipe[0]; - String batchId = createPlannedBatch(orderAndRecipe[1]); - - String json = """ - {"batchId": "%s"} - """.formatted(batchId); + @DisplayName("RELEASED Order starten → 200, Batch automatisch erstellt, Status IN_PROGRESS") + void startOrder_released_returns200() throws Exception { + String orderId = createReleasedOrder(); mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(orderId)) .andExpect(jsonPath("$.status").value("IN_PROGRESS")) - .andExpect(jsonPath("$.batchId").value(batchId)); + .andExpect(jsonPath("$.batchId").isNotEmpty()) + .andExpect(jsonPath("$.batchNumber").isNotEmpty()); } @Test @DisplayName("PLANNED Order starten → 409 (InvalidStatusTransition)") void startOrder_plannedOrder_returns409() throws Exception { - String[] orderAndRecipe = createPlannedOrderWithRecipe(); - String orderId = orderAndRecipe[0]; - String batchId = createPlannedBatch(orderAndRecipe[1]); - - String json = """ - {"batchId": "%s"} - """.formatted(batchId); + String orderId = createPlannedOrder(); mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isConflict()) .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_STATUS_TRANSITION")); } @@ -431,145 +416,54 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { @Test @DisplayName("Order nicht gefunden → 404") void startOrder_notFound_returns404() throws Exception { - String json = """ - {"batchId": "non-existent-batch"} - """; - mockMvc.perform(post("/api/production/production-orders/{id}/start", "non-existent-id") - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_NOT_FOUND")); } - @Test - @DisplayName("Batch nicht gefunden → 400") - void startOrder_batchNotFound_returns400() throws Exception { - String orderId = createReleasedOrder(); - - String json = """ - {"batchId": "non-existent-batch"} - """; - - mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_VALIDATION_ERROR")); - } - @Test @DisplayName("Ohne PRODUCTION_ORDER_WRITE → 403") void startOrder_withViewerToken_returns403() throws Exception { - String json = """ - {"batchId": "any-batch"} - """; - mockMvc.perform(post("/api/production/production-orders/{id}/start", "any-id") - .header("Authorization", "Bearer " + viewerToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + .header("Authorization", "Bearer " + viewerToken)) .andExpect(status().isForbidden()); } @Test @DisplayName("Ohne Token → 401") void startOrder_withoutToken_returns401() throws Exception { - String json = """ - {"batchId": "any-batch"} - """; - - mockMvc.perform(post("/api/production/production-orders/{id}/start", "any-id") - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + mockMvc.perform(post("/api/production/production-orders/{id}/start", "any-id")) .andExpect(status().isUnauthorized()); } - @Test - @DisplayName("Batch nicht PLANNED (bereits gestartet) → 400") - void startOrder_batchNotPlanned_returns400() throws Exception { - String[] orderAndRecipe = createReleasedOrderWithRecipe(); - String orderId = orderAndRecipe[0]; - String batchId = createStartedBatch(orderAndRecipe[1]); - - String json = """ - {"batchId": "%s"} - """.formatted(batchId); - - mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_VALIDATION_ERROR")); - } - @Test @DisplayName("Bereits gestartete Order erneut starten → 409") void startOrder_alreadyStarted_returns409() throws Exception { - String[] orderAndRecipe = createReleasedOrderWithRecipe(); - String orderId = orderAndRecipe[0]; - String recipeId = orderAndRecipe[1]; - String batchId1 = createPlannedBatch(recipeId); - String batchId2 = createPlannedBatch(recipeId); - - String json1 = """ - {"batchId": "%s"} - """.formatted(batchId1); + String orderId = createReleasedOrder(); // First start mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json1)) + .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()); // Second start - String json2 = """ - {"batchId": "%s"} - """.formatted(batchId2); - mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json2)) + .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isConflict()) .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_STATUS_TRANSITION")); } @Test - @DisplayName("Batch mit anderem Rezept → 400 (RecipeMismatch)") - void startOrder_recipeMismatch_returns400() throws Exception { - String orderId = createReleasedOrder(); - String batchId = createPlannedBatch(); // creates batch with different recipe - - String json = """ - {"batchId": "%s"} - """.formatted(batchId); + @DisplayName("Order mit archiviertem Rezept starten → 400") + void startOrder_archivedRecipe_returns400() throws Exception { + String orderId = createReleasedOrderThenArchiveRecipe(); mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_VALIDATION_ERROR")) - .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("does not match order recipe"))); - } - - @Test - @DisplayName("batchId leer → 400 (Bean Validation)") - void startOrder_blankBatchId_returns400() throws Exception { - String json = """ - {"batchId": ""} - """; - - mockMvc.perform(post("/api/production/production-orders/{id}/start", "any-id") - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) - .andExpect(status().isBadRequest()); + .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("not ACTIVE"))); } } @@ -1178,21 +1072,12 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { return orderAndRecipe; } - /** Creates an IN_PROGRESS order (with a started batch). Returns orderId. */ + /** Creates an IN_PROGRESS order (batch auto-created by start). Returns orderId. */ private String createInProgressOrder() throws Exception { - String[] orderAndRecipe = createReleasedOrderWithRecipe(); - String orderId = orderAndRecipe[0]; - String recipeId = orderAndRecipe[1]; - String batchId = createPlannedBatch(recipeId); - - String json = """ - {"batchId": "%s"} - """.formatted(batchId); + String orderId = createReleasedOrder(); mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(json)) + .header("Authorization", "Bearer " + adminToken)) .andExpect(status().isOk()); return orderId; @@ -1203,17 +1088,13 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { String[] orderAndRecipe = createReleasedOrderWithRecipe(); String orderId = orderAndRecipe[0]; String recipeId = orderAndRecipe[1]; - String batchId = createPlannedBatch(recipeId); - String startJson = """ - {"batchId": "%s"} - """.formatted(batchId); + var startResult = mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()) + .andReturn(); - mockMvc.perform(post("/api/production/production-orders/{id}/start", orderId) - .header("Authorization", "Bearer " + adminToken) - .contentType(MediaType.APPLICATION_JSON) - .content(startJson)) - .andExpect(status().isOk()); + String batchId = objectMapper.readTree(startResult.getResponse().getContentAsString()).get("batchId").asText(); // Record a consumption (required to complete batch) String inputBatchId = createPlannedBatch(recipeId); @@ -1236,8 +1117,18 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { return new String[]{orderId, batchId}; } - private String createPlannedBatch() throws Exception { - return createPlannedBatch(createActiveRecipe()); + /** Creates a RELEASED order, then archives its recipe. Returns orderId. */ + private String createReleasedOrderThenArchiveRecipe() throws Exception { + String[] orderAndRecipe = createReleasedOrderWithRecipe(); + String orderId = orderAndRecipe[0]; + String recipeId = orderAndRecipe[1]; + + // Archive the recipe + mockMvc.perform(post("/api/recipes/{id}/archive", recipeId) + .header("Authorization", "Bearer " + adminToken)) + .andExpect(status().isOk()); + + return orderId; } private String createPlannedBatch(String recipeId) throws Exception { @@ -1252,18 +1143,6 @@ class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest { return objectMapper.readTree(planResult.getResponse().getContentAsString()).get("id").asText(); } - private String createStartedBatch() throws Exception { - return createStartedBatch(createActiveRecipe()); - } - - private String createStartedBatch(String recipeId) throws Exception { - String batchId = createPlannedBatch(recipeId); - mockMvc.perform(post("/api/production/batches/{id}/start", batchId) - .header("Authorization", "Bearer " + adminToken)) - .andExpect(status().isOk()); - return batchId; - } - private String createDraftRecipe() throws Exception { String json = """ { diff --git a/frontend/apps/cli/src/components/production/ProductionOrderDetailScreen.tsx b/frontend/apps/cli/src/components/production/ProductionOrderDetailScreen.tsx index 4970021..9a702c2 100644 --- a/frontend/apps/cli/src/components/production/ProductionOrderDetailScreen.tsx +++ b/frontend/apps/cli/src/components/production/ProductionOrderDetailScreen.tsx @@ -20,7 +20,7 @@ const STATUS_COLORS: Record = { CANCELLED: 'red', }; -type Mode = 'view' | 'menu' | 'start-batch-input' | 'reschedule-input'; +type Mode = 'view' | 'menu' | 'reschedule-input'; export function ProductionOrderDetailScreen() { const { params, back } = useNavigation(); @@ -31,7 +31,6 @@ export function ProductionOrderDetailScreen() { const { recipeName } = useRecipeNameLookup(); const [mode, setMode] = useState('view'); const [menuIndex, setMenuIndex] = useState(0); - const [batchId, setBatchId] = useState(''); const [newDate, setNewDate] = useState(''); const [success, setSuccess] = useState(null); const [batch, setBatch] = useState(null); @@ -80,12 +79,11 @@ export function ProductionOrderDetailScreen() { }; const handleStart = async () => { - if (!batchId.trim()) return; - const result = await startProductionOrder(orderId, { batchId: batchId.trim() }); + const result = await startProductionOrder(orderId); if (result) { - setSuccess('Produktion gestartet.'); + const bn = result.batchNumber ? ` Charge: ${result.batchNumber}` : ''; + setSuccess(`Produktion gestartet.${bn}`); setMode('view'); - setBatchId(''); } }; @@ -107,11 +105,6 @@ export function ProductionOrderDetailScreen() { return; } - if (mode === 'start-batch-input') { - if (key.escape) setMode('menu'); - return; - } - if (mode === 'menu') { if (key.upArrow) setMenuIndex((i) => Math.max(0, i - 1)); if (key.downArrow) setMenuIndex((i) => Math.min(menuItems.length - 1, i + 1)); @@ -122,10 +115,7 @@ export function ProductionOrderDetailScreen() { setMode('reschedule-input'); setNewDate(''); } - if (action === 'start') { - setMode('start-batch-input'); - setBatchId(''); - } + if (action === 'start') void handleStart(); } if (key.escape) setMode('view'); return; @@ -218,22 +208,6 @@ export function ProductionOrderDetailScreen() { )} - {mode === 'start-batch-input' && ( - - Chargen-ID eingeben: - - - void handleStart()} - focus={true} - /> - - Enter bestätigen · Escape abbrechen - - )} - {mode === 'reschedule-input' && ( Neues Datum (YYYY-MM-DD): diff --git a/frontend/apps/cli/src/hooks/useProductionOrders.ts b/frontend/apps/cli/src/hooks/useProductionOrders.ts index d202017..e76f70a 100644 --- a/frontend/apps/cli/src/hooks/useProductionOrders.ts +++ b/frontend/apps/cli/src/hooks/useProductionOrders.ts @@ -1,5 +1,5 @@ import { useState, useCallback } from 'react'; -import type { ProductionOrderDTO, CreateProductionOrderRequest, StartProductionOrderRequest, ProductionOrderFilter } from '@effigenix/api-client'; +import type { ProductionOrderDTO, CreateProductionOrderRequest, ProductionOrderFilter } from '@effigenix/api-client'; import { client } from '../utils/api-client.js'; interface ProductionOrdersState { @@ -77,10 +77,10 @@ export function useProductionOrders() { } }, []); - const startProductionOrder = useCallback(async (id: string, request: StartProductionOrderRequest) => { + const startProductionOrder = useCallback(async (id: string) => { setState((s) => ({ ...s, loading: true, error: null })); try { - const productionOrder = await client.productionOrders.start(id, request); + const productionOrder = await client.productionOrders.start(id); setState((s) => ({ ...s, productionOrder, loading: false, error: null })); return productionOrder; } catch (err) { diff --git a/frontend/openapi.json b/frontend/openapi.json index 2c27865..4294525 100644 --- a/frontend/openapi.json +++ b/frontend/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Storage Locations","description":"Storage location management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Countries","description":"ISO 3166-1 country reference data"},{"name":"Recipes","description":"Recipe management endpoints"},{"name":"Batches","description":"Production batch management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Stock Movements","description":"Stock movement tracking endpoints"},{"name":"Product Categories","description":"Product category management endpoints"},{"name":"Articles","description":"Article management endpoints"},{"name":"Stocks","description":"Stock management endpoints"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"},{"name":"Production Orders","description":"Production order management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId).","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}":{"get":{"tags":["Storage Locations"],"operationId":"getStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Storage Locations"],"operationId":"updateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{id}":{"get":{"tags":["Stocks"],"operationId":"getStock","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Stocks"],"operationId":"updateStock","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login).","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login).","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupplierResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes":{"get":{"tags":["Recipes"],"operationId":"listRecipes","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RecipeSummaryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Recipes"],"operationId":"createRecipe","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecipeRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps":{"post":{"tags":["Recipes"],"operationId":"addProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProductionStepRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients":{"post":{"tags":["Recipes"],"operationId":"addIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipeIngredientRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/archive":{"post":{"tags":["Recipes"],"operationId":"archiveRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/activate":{"post":{"tags":["Recipes"],"operationId":"activateRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders":{"get":{"tags":["Production Orders"],"operationId":"listProductionOrders","parameters":[{"name":"dateFrom","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"dateTo","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["PLANNED","RELEASED","IN_PROGRESS","COMPLETED","CANCELLED"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Production Orders"],"operationId":"createProductionOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductionOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/start":{"post":{"tags":["Production Orders"],"operationId":"startProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartProductionOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/reschedule":{"post":{"tags":["Production Orders"],"operationId":"rescheduleProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleProductionOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/release":{"post":{"tags":["Production Orders"],"operationId":"releaseProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/complete":{"post":{"tags":["Production Orders"],"operationId":"completeProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/cancel":{"post":{"tags":["Production Orders"],"operationId":"cancelProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelProductionOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches":{"get":{"tags":["Batches"],"operationId":"listBatches","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"productionDate","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BatchSummaryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Batches"],"operationId":"planBatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/start":{"post":{"tags":["Batches"],"operationId":"startBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/consumptions":{"post":{"tags":["Batches"],"operationId":"recordConsumption","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordConsumptionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConsumptionResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/complete":{"post":{"tags":["Batches"],"operationId":"completeBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/cancel":{"post":{"tags":["Batches"],"operationId":"cancelBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations":{"get":{"tags":["Storage Locations"],"operationId":"listStorageLocations","parameters":[{"name":"storageType","in":"query","required":false,"schema":{"type":"string"}},{"name":"active","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Storage Locations"],"operationId":"createStorageLocation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks":{"get":{"tags":["Stocks"],"operationId":"listStocks","parameters":[{"name":"storageLocationId","in":"query","required":false,"schema":{"type":"string"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Stocks"],"operationId":"createStock","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CreateStockResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/reservations":{"post":{"tags":["Stocks"],"operationId":"reserveStock","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReserveStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReservationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm":{"post":{"tags":["Stocks"],"operationId":"confirmReservation","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reservationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches":{"post":{"tags":["Stocks"],"operationId":"addBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockBatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/unblock":{"post":{"tags":["Stocks"],"operationId":"unblockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/remove":{"post":{"tags":["Stocks"],"operationId":"removeBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/block":{"post":{"tags":["Stocks"],"operationId":"blockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stock-movements":{"get":{"tags":["Stock Movements"],"summary":"List stock movements","description":"Filter priority (only one filter applied): stockId > articleId > batchReference > movementType > from/to","operationId":"listMovements","parameters":[{"name":"stockId","in":"query","required":false,"schema":{"type":"string"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}},{"name":"movementType","in":"query","required":false,"schema":{"type":"string"}},{"name":"batchReference","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockMovementResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Stock Movements"],"operationId":"recordMovement","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordStockMovementRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockMovementResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT tokens.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"429":{"description":"Too many login attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/deactivate":{"patch":{"tags":["Storage Locations"],"operationId":"deactivateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/activate":{"patch":{"tags":["Storage Locations"],"operationId":"activateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}":{"get":{"tags":["Recipes"],"operationId":"getRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}":{"get":{"tags":["Production Orders"],"operationId":"getProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}":{"get":{"tags":["Batches"],"operationId":"getBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/by-number/{batchNumber}":{"get":{"tags":["Batches"],"operationId":"findByNumber","parameters":[{"name":"batchNumber","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/below-minimum":{"get":{"tags":["Stocks"],"operationId":"listStocksBelowMinimum","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockResponse"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stock-movements/{id}":{"get":{"tags":["Stock Movements"],"operationId":"getMovement","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockMovementResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/countries":{"get":{"tags":["Countries"],"operationId":"search","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":""}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CountryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}}],"responses":{"403":{"description":"Missing permission"},"404":{"description":"User or role not found"},"204":{"description":"Role removed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps/{stepNumber}":{"delete":{"tags":["Recipes"],"operationId":"removeProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"stepNumber","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients/{ingredientId}":{"delete":{"tags":["Recipes"],"operationId":"removeIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"ingredientId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/reservations/{reservationId}":{"delete":{"tags":["Stocks"],"operationId":"releaseReservation","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reservationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"required":["description","id","name","permissions"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_CANCEL","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","MASTERDATA_READ","MASTERDATA_WRITE","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"required":["createdAt","email","id","roles","status","username"],"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddressResponse":{"required":["city","country","houseNumber","postalCode","street"],"type":"object","properties":{"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"}},"nullable":true},"ContactInfoResponse":{"required":["contactPerson","email","phone"],"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"}}},"PaymentTermsResponse":{"required":["paymentDescription","paymentDueDays"],"type":"object","properties":{"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}},"nullable":true},"QualityCertificateResponse":{"required":["certificateType","issuer","validFrom","validUntil"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"SupplierRatingResponse":{"required":["deliveryScore","priceScore","qualityScore"],"type":"object","properties":{"qualityScore":{"type":"integer","format":"int32"},"deliveryScore":{"type":"integer","format":"int32"},"priceScore":{"type":"integer","format":"int32"}},"nullable":true},"SupplierResponse":{"required":["certificates","contactInfo","createdAt","id","name","status","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"certificates":{"type":"array","items":{"$ref":"#/components/schemas/QualityCertificateResponse"}},"rating":{"$ref":"#/components/schemas/SupplierRatingResponse"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateStorageLocationRequest":{"type":"object","properties":{"name":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"StorageLocationResponse":{"required":["active","id","name","storageType"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"storageType":{"type":"string"},"temperatureRange":{"$ref":"#/components/schemas/TemperatureRangeResponse"},"active":{"type":"boolean"}}},"TemperatureRangeResponse":{"required":["maxTemperature","minTemperature"],"type":"object","properties":{"minTemperature":{"type":"number"},"maxTemperature":{"type":"number"}},"nullable":true},"UpdateStockRequest":{"type":"object","properties":{"minimumLevelAmount":{"type":"string"},"minimumLevelUnit":{"type":"string"},"minimumShelfLifeDays":{"type":"integer","format":"int32"}}},"MinimumLevelResponse":{"required":["amount","unit"],"type":"object","properties":{"amount":{"type":"number"},"unit":{"type":"string"}},"nullable":true},"ReservationResponse":{"type":"object","properties":{"id":{"type":"string"},"referenceType":{"type":"string"},"referenceId":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"priority":{"type":"string"},"reservedAt":{"type":"string","format":"date-time"},"allocations":{"type":"array","items":{"$ref":"#/components/schemas/StockBatchAllocationResponse"}}}},"StockBatchAllocationResponse":{"type":"object","properties":{"stockBatchId":{"type":"string"},"allocatedQuantityAmount":{"type":"number"},"allocatedQuantityUnit":{"type":"string"}}},"StockBatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string","format":"date"},"status":{"type":"string"},"receivedAt":{"type":"string","format":"date-time"}}},"StockResponse":{"required":["articleId","availableQuantity","batches","id","reservations","storageLocationId","totalQuantity"],"type":"object","properties":{"id":{"type":"string"},"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevel":{"$ref":"#/components/schemas/MinimumLevelResponse"},"minimumShelfLifeDays":{"type":"integer","format":"int32","nullable":true},"batches":{"type":"array","items":{"$ref":"#/components/schemas/StockBatchResponse"}},"totalQuantity":{"type":"number"},"quantityUnit":{"type":"string","nullable":true},"availableQuantity":{"type":"number"},"reservations":{"type":"array","items":{"$ref":"#/components/schemas/ReservationResponse"}}}},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"ContractLineItemResponse":{"required":["agreedPrice","agreedQuantity","articleId","unit"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string"}}},"CustomerResponse":{"required":["billingAddress","contactInfo","createdAt","deliveryAddresses","id","name","preferences","status","type","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"billingAddress":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"deliveryAddresses":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAddressResponse"}},"frameContract":{"$ref":"#/components/schemas/FrameContractResponse"},"preferences":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DeliveryAddressResponse":{"required":["address","contactPerson","deliveryNotes","label"],"type":"object","properties":{"label":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"FrameContractResponse":{"required":["deliveryRhythm","id","lineItems","validFrom","validUntil"],"type":"object","properties":{"id":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"deliveryRhythm":{"type":"string"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/ContractLineItemResponse"}}},"nullable":true},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategoryResponse":{"required":["description","id","name"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"ArticleResponse":{"required":["articleNumber","categoryId","createdAt","id","name","salesUnits","status","supplierIds","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"salesUnits":{"type":"array","items":{"$ref":"#/components/schemas/SalesUnitResponse"}},"status":{"type":"string"},"supplierIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SalesUnitResponse":{"required":["id","price","priceModel","unit"],"type":"object","properties":{"id":{"type":"string"},"unit":{"type":"string"},"priceModel":{"type":"string"},"price":{"type":"number"}}},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateRecipeRequest":{"required":["articleId","name","outputQuantity","outputUom","type"],"type":"object","properties":{"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string","enum":["RAW_MATERIAL","INTERMEDIATE","FINISHED_PRODUCT"]},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32"},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"}}},"IngredientResponse":{"required":["articleId","id","position","quantity","substitutable","uom"],"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string","nullable":true},"substitutable":{"type":"boolean"}}},"ProductionStepResponse":{"required":["description","id","stepNumber"],"type":"object","properties":{"id":{"type":"string"},"stepNumber":{"type":"integer","format":"int32"},"description":{"type":"string"},"durationMinutes":{"type":"integer","format":"int32","nullable":true},"temperatureCelsius":{"type":"integer","format":"int32","nullable":true}}},"RecipeResponse":{"required":["articleId","createdAt","description","id","ingredients","name","outputQuantity","outputUom","productionSteps","status","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/IngredientResponse"}},"productionSteps":{"type":"array","items":{"$ref":"#/components/schemas/ProductionStepResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AddProductionStepRequest":{"required":["description"],"type":"object","properties":{"stepNumber":{"minimum":1,"type":"integer","format":"int32"},"description":{"maxLength":500,"minLength":0,"type":"string"},"durationMinutes":{"minimum":1,"type":"integer","format":"int32"},"temperatureCelsius":{"maximum":1000,"minimum":-273,"type":"integer","format":"int32"}}},"AddRecipeIngredientRequest":{"required":["articleId","quantity","uom"],"type":"object","properties":{"position":{"minimum":1,"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string"},"substitutable":{"type":"boolean"}}},"CreateProductionOrderRequest":{"required":["plannedDate","plannedQuantity","plannedQuantityUnit","priority","recipeId"],"type":"object","properties":{"recipeId":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"plannedDate":{"type":"string","format":"date"},"priority":{"type":"string"},"notes":{"type":"string"}}},"ProductionOrderResponse":{"type":"object","properties":{"id":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"batchId":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"plannedDate":{"type":"string","format":"date"},"priority":{"type":"string"},"notes":{"type":"string"},"cancelledReason":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"StartProductionOrderRequest":{"required":["batchId"],"type":"object","properties":{"batchId":{"type":"string"}}},"RescheduleProductionOrderRequest":{"required":["newPlannedDate"],"type":"object","properties":{"newPlannedDate":{"type":"string","format":"date"}}},"CancelProductionOrderRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"PlanBatchRequest":{"required":["bestBeforeDate","plannedQuantity","plannedQuantityUnit","productionDate","recipeId"],"type":"object","properties":{"recipeId":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"}}},"BatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchNumber":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"actualQuantity":{"type":"string"},"actualQuantityUnit":{"type":"string"},"waste":{"type":"string"},"wasteUnit":{"type":"string"},"remarks":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"},"consumptions":{"type":"array","items":{"$ref":"#/components/schemas/ConsumptionResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"cancellationReason":{"type":"string"},"cancelledAt":{"type":"string","format":"date-time"}}},"ConsumptionResponse":{"type":"object","properties":{"id":{"type":"string"},"inputBatchId":{"type":"string"},"articleId":{"type":"string"},"quantityUsed":{"type":"string"},"quantityUsedUnit":{"type":"string"},"consumedAt":{"type":"string","format":"date-time"}}},"RecordConsumptionRequest":{"required":["articleId","inputBatchId","quantityUnit","quantityUsed"],"type":"object","properties":{"inputBatchId":{"type":"string"},"articleId":{"type":"string"},"quantityUsed":{"type":"string"},"quantityUnit":{"type":"string"}}},"CompleteBatchRequest":{"required":["actualQuantity","actualQuantityUnit","waste","wasteUnit"],"type":"object","properties":{"actualQuantity":{"type":"string"},"actualQuantityUnit":{"type":"string"},"waste":{"type":"string"},"wasteUnit":{"type":"string"},"remarks":{"type":"string"}}},"CancelBatchRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"CreateStorageLocationRequest":{"required":["name","storageType"],"type":"object","properties":{"name":{"type":"string"},"storageType":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"CreateStockRequest":{"required":["articleId","storageLocationId"],"type":"object","properties":{"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevelAmount":{"type":"string"},"minimumLevelUnit":{"type":"string"},"minimumShelfLifeDays":{"type":"integer","format":"int32"}}},"CreateStockResponse":{"required":["articleId","id","storageLocationId"],"type":"object","properties":{"id":{"type":"string"},"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevel":{"$ref":"#/components/schemas/MinimumLevelResponse"},"minimumShelfLifeDays":{"type":"integer","format":"int32","nullable":true}}},"ReserveStockRequest":{"required":["priority","quantityAmount","quantityUnit","referenceId","referenceType"],"type":"object","properties":{"referenceType":{"type":"string"},"referenceId":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"priority":{"type":"string"}}},"AddStockBatchRequest":{"required":["batchId","batchType","expiryDate","quantityAmount","quantityUnit"],"type":"object","properties":{"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string"}}},"RemoveStockBatchRequest":{"required":["quantityAmount","quantityUnit"],"type":"object","properties":{"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"}}},"BlockStockBatchRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"RecordStockMovementRequest":{"required":["articleId","batchId","batchType","movementType","quantityAmount","quantityUnit","stockBatchId","stockId"],"type":"object","properties":{"stockId":{"type":"string"},"articleId":{"type":"string"},"stockBatchId":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"movementType":{"type":"string"},"direction":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"reason":{"type":"string"},"referenceDocumentId":{"type":"string"}}},"StockMovementResponse":{"required":["articleId","batchId","batchType","direction","id","movementType","performedAt","performedBy","quantityAmount","quantityUnit","stockBatchId","stockId"],"type":"object","properties":{"id":{"type":"string"},"stockId":{"type":"string"},"articleId":{"type":"string"},"stockBatchId":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"movementType":{"type":"string"},"direction":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"reason":{"type":"string","nullable":true},"referenceDocumentId":{"type":"string","nullable":true},"performedBy":{"type":"string"},"performedAt":{"type":"string","format":"date-time"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"required":["accessToken","expiresAt","expiresIn","refreshToken","tokenType"],"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RecipeSummaryResponse":{"required":["articleId","createdAt","description","id","ingredientCount","name","outputQuantity","outputUom","status","stepCount","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredientCount":{"type":"integer","format":"int32"},"stepCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"BatchSummaryResponse":{"type":"object","properties":{"id":{"type":"string"},"batchNumber":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"CountryResponse":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"Effigenix Fleischerei ERP API","description":"RESTful API for Effigenix Fleischerei ERP System.\n\n## Authentication\n\nAll endpoints (except /api/auth/login and /api/auth/refresh) require JWT authentication.\n\n1. Login via POST /api/auth/login with username and password\n2. Copy the returned access token\n3. Click \"Authorize\" button (top right)\n4. Enter: Bearer \n5. Click \"Authorize\"\n\n## User Management\n\n- **Authentication**: Login, logout, refresh token\n- **User Management**: Create, update, list users (ADMIN only)\n- **Role Management**: Assign roles, lock/unlock users (ADMIN only)\n- **Password Management**: Change password (requires current password)\n\n## Error Handling\n\nAll errors return a consistent error response format:\n\n```json\n{\n \"code\": \"USER_NOT_FOUND\",\n \"message\": \"User with ID 'user-123' not found\",\n \"status\": 404,\n \"timestamp\": \"2026-02-17T12:00:00\",\n \"path\": \"/api/users/user-123\",\n \"validationErrors\": null\n}\n```\n\n## Architecture\n\nBuilt with:\n- Domain-Driven Design (DDD)\n- Clean Architecture (Hexagonal Architecture)\n- Spring Boot 3.2\n- Java 21\n- PostgreSQL\n","contact":{"name":"Effigenix Development Team","url":"https://effigenix.com","email":"dev@effigenix.com"},"license":{"name":"Proprietary","url":"https://effigenix.com/license"},"version":"0.1.0"},"servers":[{"url":"http://localhost:8080","description":"Local Development Server"},{"url":"https://api.effigenix.com","description":"Production Server"}],"tags":[{"name":"Storage Locations","description":"Storage location management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Countries","description":"ISO 3166-1 country reference data"},{"name":"Recipes","description":"Recipe management endpoints"},{"name":"Batches","description":"Production batch management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Stock Movements","description":"Stock movement tracking endpoints"},{"name":"Product Categories","description":"Product category management endpoints"},{"name":"Articles","description":"Article management endpoints"},{"name":"Stocks","description":"Stock management endpoints"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Authentication","description":"Authentication and session management endpoints"},{"name":"Production Orders","description":"Production order management endpoints"}],"paths":{"/api/users/{id}":{"get":{"tags":["User Management"],"summary":"Get user by ID","description":"Retrieve a single user by their ID.","operationId":"getUserById","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["User Management"],"summary":"Update user","description":"Update user details (email, branchId).","operationId":"updateUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"409":{"description":"Email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/password":{"put":{"tags":["User Management"],"summary":"Change password","description":"Change user password. Requires current password for verification.","operationId":"changePassword","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"400":{"description":"Invalid password"},"401":{"description":"Invalid current password"},"404":{"description":"User not found"},"204":{"description":"Password changed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}":{"get":{"tags":["Suppliers"],"operationId":"getSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Suppliers"],"operationId":"updateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}":{"get":{"tags":["Storage Locations"],"operationId":"getStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Storage Locations"],"operationId":"updateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{id}":{"get":{"tags":["Stocks"],"operationId":"getStock","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Stocks"],"operationId":"updateStock","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}":{"get":{"tags":["Customers"],"operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Customers"],"operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/preferences":{"put":{"tags":["Customers"],"operationId":"setPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/frame-contract":{"put":{"tags":["Customers"],"operationId":"setFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetFrameContractRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Customers"],"operationId":"removeFrameContract","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/categories/{id}":{"put":{"tags":["Product Categories"],"operationId":"updateCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Product Categories"],"operationId":"deleteCategory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}":{"get":{"tags":["Articles"],"operationId":"getArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"put":{"tags":["Articles"],"operationId":"updateArticle","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}/price":{"put":{"tags":["Articles"],"operationId":"updateSalesUnitPrice","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSalesUnitPriceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users":{"get":{"tags":["User Management"],"summary":"List all users","description":"Get a list of all users in the system.","operationId":"listUsers","responses":{"200":{"description":"Users retrieved successfully","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDTO"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["User Management"],"summary":"Create user (ADMIN only)","description":"Create a new user account with specified roles.","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"201":{"description":"User created successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"400":{"description":"Validation error or invalid password","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Username or email already exists","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/unlock":{"post":{"tags":["User Management"],"summary":"Unlock user (ADMIN only)","description":"Unlock a user account (allows login).","operationId":"unlockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User unlocked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles":{"post":{"tags":["User Management"],"summary":"Assign role (ADMIN only)","description":"Assign a role to a user.","operationId":"assignRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"200":{"description":"Role assigned successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User or role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/lock":{"post":{"tags":["User Management"],"summary":"Lock user (ADMIN only)","description":"Lock a user account (prevents login).","operationId":"lockUser","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}}],"responses":{"403":{"description":"Missing permission","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"200":{"description":"User locked successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Invalid status transition","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers":{"get":{"tags":["Suppliers"],"operationId":"listSuppliers","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupplierResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Suppliers"],"operationId":"createSupplier","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/rating":{"post":{"tags":["Suppliers"],"operationId":"rateSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"operationId":"deactivate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/certificates":{"post":{"tags":["Suppliers"],"operationId":"addCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]},"delete":{"tags":["Suppliers"],"operationId":"removeCertificate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCertificateRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"operationId":"activate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SupplierResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes":{"get":{"tags":["Recipes"],"operationId":"listRecipes","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RecipeSummaryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Recipes"],"operationId":"createRecipe","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRecipeRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps":{"post":{"tags":["Recipes"],"operationId":"addProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProductionStepRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients":{"post":{"tags":["Recipes"],"operationId":"addIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipeIngredientRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/archive":{"post":{"tags":["Recipes"],"operationId":"archiveRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/activate":{"post":{"tags":["Recipes"],"operationId":"activateRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders":{"get":{"tags":["Production Orders"],"operationId":"listProductionOrders","parameters":[{"name":"dateFrom","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"dateTo","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["PLANNED","RELEASED","IN_PROGRESS","COMPLETED","CANCELLED"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Production Orders"],"operationId":"createProductionOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductionOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/start":{"post":{"tags":["Production Orders"],"operationId":"startProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/reschedule":{"post":{"tags":["Production Orders"],"operationId":"rescheduleProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleProductionOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/release":{"post":{"tags":["Production Orders"],"operationId":"releaseProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/complete":{"post":{"tags":["Production Orders"],"operationId":"completeProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}/cancel":{"post":{"tags":["Production Orders"],"operationId":"cancelProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelProductionOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches":{"get":{"tags":["Batches"],"operationId":"listBatches","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"productionDate","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BatchSummaryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Batches"],"operationId":"planBatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/start":{"post":{"tags":["Batches"],"operationId":"startBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/consumptions":{"post":{"tags":["Batches"],"operationId":"recordConsumption","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordConsumptionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConsumptionResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/complete":{"post":{"tags":["Batches"],"operationId":"completeBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}/cancel":{"post":{"tags":["Batches"],"operationId":"cancelBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations":{"get":{"tags":["Storage Locations"],"operationId":"listStorageLocations","parameters":[{"name":"storageType","in":"query","required":false,"schema":{"type":"string"}},{"name":"active","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Storage Locations"],"operationId":"createStorageLocation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStorageLocationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks":{"get":{"tags":["Stocks"],"operationId":"listStocks","parameters":[{"name":"storageLocationId","in":"query","required":false,"schema":{"type":"string"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Stocks"],"operationId":"createStock","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CreateStockResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/reservations":{"post":{"tags":["Stocks"],"operationId":"reserveStock","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReserveStockRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReservationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm":{"post":{"tags":["Stocks"],"operationId":"confirmReservation","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reservationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches":{"post":{"tags":["Stocks"],"operationId":"addBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockBatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/unblock":{"post":{"tags":["Stocks"],"operationId":"unblockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/remove":{"post":{"tags":["Stocks"],"operationId":"removeBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/batches/{batchId}/block":{"post":{"tags":["Stocks"],"operationId":"blockBatch","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockStockBatchRequest"}}},"required":true},"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stock-movements":{"get":{"tags":["Stock Movements"],"summary":"List stock movements","description":"Filter priority (only one filter applied): stockId > articleId > batchReference > movementType > from/to","operationId":"listMovements","parameters":[{"name":"stockId","in":"query","required":false,"schema":{"type":"string"}},{"name":"articleId","in":"query","required":false,"schema":{"type":"string"}},{"name":"movementType","in":"query","required":false,"schema":{"type":"string"}},{"name":"batchReference","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockMovementResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Stock Movements"],"operationId":"recordMovement","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordStockMovementRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockMovementResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers":{"get":{"tags":["Customers"],"operationId":"listCustomers","parameters":[{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["B2C","B2B"]}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Customers"],"operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses":{"post":{"tags":["Customers"],"operationId":"addDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDeliveryAddressRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/deactivate":{"post":{"tags":["Customers"],"operationId":"deactivate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/activate":{"post":{"tags":["Customers"],"operationId":"activate_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/categories":{"get":{"tags":["Product Categories"],"operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Product Categories"],"operationId":"createCategory","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductCategoryRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductCategoryResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","description":"Refresh an expired access token using a valid refresh token.","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Token refresh successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/auth/logout":{"post":{"tags":["Authentication"],"summary":"User logout","description":"Invalidate current JWT token.","operationId":"logout","responses":{"401":{"description":"Invalid or missing authentication token"},"204":{"description":"Logout successful"}}}},"/api/auth/login":{"post":{"tags":["Authentication"],"summary":"User login","description":"Authenticate user with username and password. Returns JWT tokens.","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"429":{"description":"Too many login attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"401":{"description":"Invalid credentials, user locked, or user inactive","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}}}}},"/api/articles":{"get":{"tags":["Articles"],"operationId":"listArticles","parameters":[{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"}}}}}},"security":[{"Bearer Authentication":[]}]},"post":{"tags":["Articles"],"operationId":"createArticle","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateArticleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers":{"post":{"tags":["Articles"],"operationId":"assignSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignSupplierRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units":{"post":{"tags":["Articles"],"operationId":"addSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddSalesUnitRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/deactivate":{"post":{"tags":["Articles"],"operationId":"deactivate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/activate":{"post":{"tags":["Articles"],"operationId":"activate_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ArticleResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/deactivate":{"patch":{"tags":["Storage Locations"],"operationId":"deactivateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/storage-locations/{id}/activate":{"patch":{"tags":["Storage Locations"],"operationId":"activateStorageLocation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StorageLocationResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/roles":{"get":{"tags":["Role Management"],"summary":"List all roles (ADMIN only)","description":"Get a list of all available roles in the system. Requires USER_MANAGEMENT permission.","operationId":"listRoles","responses":{"200":{"description":"Roles retrieved successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDTO"}}}},"403":{"description":"Missing USER_MANAGEMENT permission","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}},"401":{"description":"Authentication required","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}":{"get":{"tags":["Recipes"],"operationId":"getRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RecipeResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/production-orders/{id}":{"get":{"tags":["Production Orders"],"operationId":"getProductionOrder","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ProductionOrderResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/{id}":{"get":{"tags":["Batches"],"operationId":"getBatch","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/production/batches/by-number/{batchNumber}":{"get":{"tags":["Batches"],"operationId":"findByNumber","parameters":[{"name":"batchNumber","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/BatchResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/below-minimum":{"get":{"tags":["Stocks"],"operationId":"listStocksBelowMinimum","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StockResponse"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stock-movements/{id}":{"get":{"tags":["Stock Movements"],"operationId":"getMovement","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StockMovementResponse"}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/countries":{"get":{"tags":["Countries"],"operationId":"search","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","default":""}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CountryResponse"}}}}}},"security":[{"Bearer Authentication":[]}]}},"/api/users/{id}/roles/{roleName}":{"delete":{"tags":["User Management"],"summary":"Remove role (ADMIN only)","description":"Remove a role from a user.","operationId":"removeRole","parameters":[{"name":"id","in":"path","description":"User ID","required":true,"schema":{"type":"string"}},{"name":"roleName","in":"path","description":"Role name","required":true,"schema":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}}],"responses":{"403":{"description":"Missing permission"},"404":{"description":"User or role not found"},"204":{"description":"Role removed successfully"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/steps/{stepNumber}":{"delete":{"tags":["Recipes"],"operationId":"removeProductionStep","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"stepNumber","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/recipes/{id}/ingredients/{ingredientId}":{"delete":{"tags":["Recipes"],"operationId":"removeIngredient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"ingredientId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/inventory/stocks/{stockId}/reservations/{reservationId}":{"delete":{"tags":["Stocks"],"operationId":"releaseReservation","parameters":[{"name":"stockId","in":"path","required":true,"schema":{"type":"string"}},{"name":"reservationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/customers/{id}/delivery-addresses/{label}":{"delete":{"tags":["Customers"],"operationId":"removeDeliveryAddress","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"label","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/suppliers/{supplierId}":{"delete":{"tags":["Articles"],"operationId":"removeSupplier","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}},"/api/articles/{id}/sales-units/{suId}":{"delete":{"tags":["Articles"],"operationId":"removeSalesUnit","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"suId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}},"security":[{"Bearer Authentication":[]}]}}},"components":{"schemas":{"UpdateUserRequest":{"type":"object","properties":{"email":{"type":"string","description":"New email address","example":"newemail@example.com"},"branchId":{"type":"string","description":"New branch ID","example":"BRANCH-002"}},"description":"Request to update user details"},"RoleDTO":{"required":["description","id","name","permissions"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]},"permissions":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["RECIPE_READ","RECIPE_WRITE","RECIPE_DELETE","BATCH_READ","BATCH_WRITE","BATCH_COMPLETE","BATCH_CANCEL","BATCH_DELETE","PRODUCTION_ORDER_READ","PRODUCTION_ORDER_WRITE","PRODUCTION_ORDER_DELETE","HACCP_READ","HACCP_WRITE","TEMPERATURE_LOG_READ","TEMPERATURE_LOG_WRITE","CLEANING_RECORD_READ","CLEANING_RECORD_WRITE","GOODS_INSPECTION_READ","GOODS_INSPECTION_WRITE","STOCK_READ","STOCK_WRITE","STOCK_MOVEMENT_READ","STOCK_MOVEMENT_WRITE","INVENTORY_COUNT_READ","INVENTORY_COUNT_WRITE","PURCHASE_ORDER_READ","PURCHASE_ORDER_WRITE","PURCHASE_ORDER_DELETE","GOODS_RECEIPT_READ","GOODS_RECEIPT_WRITE","SUPPLIER_READ","SUPPLIER_WRITE","SUPPLIER_DELETE","ORDER_READ","ORDER_WRITE","ORDER_DELETE","INVOICE_READ","INVOICE_WRITE","INVOICE_DELETE","CUSTOMER_READ","CUSTOMER_WRITE","CUSTOMER_DELETE","LABEL_READ","LABEL_WRITE","LABEL_PRINT","MASTERDATA_READ","MASTERDATA_WRITE","BRANCH_READ","BRANCH_WRITE","BRANCH_DELETE","USER_READ","USER_WRITE","USER_DELETE","USER_LOCK","USER_UNLOCK","ROLE_READ","ROLE_WRITE","ROLE_ASSIGN","ROLE_REMOVE","REPORT_READ","REPORT_GENERATE","NOTIFICATION_READ","NOTIFICATION_SEND","AUDIT_LOG_READ","SYSTEM_SETTINGS_READ","SYSTEM_SETTINGS_WRITE"]}},"description":{"type":"string"}}},"UserDTO":{"required":["createdAt","email","id","roles","status","username"],"type":"object","properties":{"id":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"roles":{"uniqueItems":true,"type":"array","items":{"$ref":"#/components/schemas/RoleDTO"}},"branchId":{"type":"string"},"status":{"type":"string","enum":["ACTIVE","INACTIVE","LOCKED"]},"createdAt":{"type":"string","format":"date-time"},"lastLogin":{"type":"string","format":"date-time"}}},"ChangePasswordRequest":{"required":["currentPassword","newPassword"],"type":"object","properties":{"currentPassword":{"type":"string","description":"Current password","example":"OldPass123"},"newPassword":{"maxLength":2147483647,"minLength":8,"type":"string","description":"New password (min 8 characters)","example":"NewSecurePass456"}},"description":"Request to change user password"},"UpdateSupplierRequest":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddressResponse":{"required":["city","country","houseNumber","postalCode","street"],"type":"object","properties":{"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"}},"nullable":true},"ContactInfoResponse":{"required":["contactPerson","email","phone"],"type":"object","properties":{"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"}}},"PaymentTermsResponse":{"required":["paymentDescription","paymentDueDays"],"type":"object","properties":{"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}},"nullable":true},"QualityCertificateResponse":{"required":["certificateType","issuer","validFrom","validUntil"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"SupplierRatingResponse":{"required":["deliveryScore","priceScore","qualityScore"],"type":"object","properties":{"qualityScore":{"type":"integer","format":"int32"},"deliveryScore":{"type":"integer","format":"int32"},"priceScore":{"type":"integer","format":"int32"}},"nullable":true},"SupplierResponse":{"required":["certificates","contactInfo","createdAt","id","name","status","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"certificates":{"type":"array","items":{"$ref":"#/components/schemas/QualityCertificateResponse"}},"rating":{"$ref":"#/components/schemas/SupplierRatingResponse"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateStorageLocationRequest":{"type":"object","properties":{"name":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"StorageLocationResponse":{"required":["active","id","name","storageType"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"storageType":{"type":"string"},"temperatureRange":{"$ref":"#/components/schemas/TemperatureRangeResponse"},"active":{"type":"boolean"}}},"TemperatureRangeResponse":{"required":["maxTemperature","minTemperature"],"type":"object","properties":{"minTemperature":{"type":"number"},"maxTemperature":{"type":"number"}},"nullable":true},"UpdateStockRequest":{"type":"object","properties":{"minimumLevelAmount":{"type":"string"},"minimumLevelUnit":{"type":"string"},"minimumShelfLifeDays":{"type":"integer","format":"int32"}}},"MinimumLevelResponse":{"required":["amount","unit"],"type":"object","properties":{"amount":{"type":"number"},"unit":{"type":"string"}},"nullable":true},"ReservationResponse":{"type":"object","properties":{"id":{"type":"string"},"referenceType":{"type":"string"},"referenceId":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"priority":{"type":"string"},"reservedAt":{"type":"string","format":"date-time"},"allocations":{"type":"array","items":{"$ref":"#/components/schemas/StockBatchAllocationResponse"}}}},"StockBatchAllocationResponse":{"type":"object","properties":{"stockBatchId":{"type":"string"},"allocatedQuantityAmount":{"type":"number"},"allocatedQuantityUnit":{"type":"string"}}},"StockBatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string","format":"date"},"status":{"type":"string"},"receivedAt":{"type":"string","format":"date-time"}}},"StockResponse":{"required":["articleId","availableQuantity","batches","id","reservations","storageLocationId","totalQuantity"],"type":"object","properties":{"id":{"type":"string"},"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevel":{"$ref":"#/components/schemas/MinimumLevelResponse"},"minimumShelfLifeDays":{"type":"integer","format":"int32","nullable":true},"batches":{"type":"array","items":{"$ref":"#/components/schemas/StockBatchResponse"}},"totalQuantity":{"type":"number"},"quantityUnit":{"type":"string","nullable":true},"availableQuantity":{"type":"number"},"reservations":{"type":"array","items":{"$ref":"#/components/schemas/ReservationResponse"}}}},"UpdateCustomerRequest":{"type":"object","properties":{"name":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"ContractLineItemResponse":{"required":["agreedPrice","agreedQuantity","articleId","unit"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string"}}},"CustomerResponse":{"required":["billingAddress","contactInfo","createdAt","deliveryAddresses","id","name","preferences","status","type","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"billingAddress":{"$ref":"#/components/schemas/AddressResponse"},"contactInfo":{"$ref":"#/components/schemas/ContactInfoResponse"},"paymentTerms":{"$ref":"#/components/schemas/PaymentTermsResponse"},"deliveryAddresses":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAddressResponse"}},"frameContract":{"$ref":"#/components/schemas/FrameContractResponse"},"preferences":{"type":"array","items":{"type":"string"}},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DeliveryAddressResponse":{"required":["address","contactPerson","deliveryNotes","label"],"type":"object","properties":{"label":{"type":"string"},"address":{"$ref":"#/components/schemas/AddressResponse"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"FrameContractResponse":{"required":["deliveryRhythm","id","lineItems","validFrom","validUntil"],"type":"object","properties":{"id":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"deliveryRhythm":{"type":"string"},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/ContractLineItemResponse"}}},"nullable":true},"SetPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"uniqueItems":true,"type":"array","items":{"type":"string","enum":["BIO","REGIONAL","TIERWOHL","HALAL","KOSHER","GLUTENFREI","LAKTOSEFREI"]}}}},"LineItem":{"required":["agreedPrice","articleId"],"type":"object","properties":{"articleId":{"type":"string"},"agreedPrice":{"type":"number"},"agreedQuantity":{"type":"number"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]}}},"SetFrameContractRequest":{"required":["lineItems","rhythm"],"type":"object","properties":{"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"},"rhythm":{"type":"string","enum":["DAILY","WEEKLY","BIWEEKLY","MONTHLY","ON_DEMAND"]},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/LineItem"}}}},"UpdateProductCategoryRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"ProductCategoryResponse":{"required":["description","id","name"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"UpdateArticleRequest":{"type":"object","properties":{"name":{"type":"string"},"categoryId":{"type":"string"}}},"ArticleResponse":{"required":["articleNumber","categoryId","createdAt","id","name","salesUnits","status","supplierIds","updatedAt"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"salesUnits":{"type":"array","items":{"$ref":"#/components/schemas/SalesUnitResponse"}},"status":{"type":"string"},"supplierIds":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SalesUnitResponse":{"required":["id","price","priceModel","unit"],"type":"object","properties":{"id":{"type":"string"},"unit":{"type":"string"},"priceModel":{"type":"string"},"price":{"type":"number"}}},"UpdateSalesUnitPriceRequest":{"required":["price"],"type":"object","properties":{"price":{"type":"number"}}},"CreateUserRequest":{"required":["email","password","roleNames","username"],"type":"object","properties":{"username":{"maxLength":50,"minLength":3,"type":"string","description":"Username (unique)","example":"john.doe"},"email":{"type":"string","description":"Email address (unique)","example":"john.doe@example.com"},"password":{"maxLength":2147483647,"minLength":8,"type":"string","description":"Password (min 8 characters)","example":"SecurePass123"},"roleNames":{"uniqueItems":true,"type":"array","description":"Role names to assign","example":["USER","MANAGER"],"items":{"type":"string","description":"Role names to assign","example":"[\"USER\",\"MANAGER\"]","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"branchId":{"type":"string","description":"Branch ID (optional)","example":"BRANCH-001"}},"description":"Request to create a new user"},"AssignRoleRequest":{"required":["roleName"],"type":"object","properties":{"roleName":{"type":"string","description":"Role name to assign","example":"MANAGER","enum":["ADMIN","PRODUCTION_MANAGER","PRODUCTION_WORKER","QUALITY_MANAGER","QUALITY_INSPECTOR","PROCUREMENT_MANAGER","WAREHOUSE_WORKER","SALES_MANAGER","SALES_STAFF"]}},"description":"Request to assign a role to a user"},"CreateSupplierRequest":{"required":["name","phone"],"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"RateSupplierRequest":{"type":"object","properties":{"qualityScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"deliveryScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"},"priceScore":{"maximum":5,"minimum":1,"type":"integer","format":"int32"}}},"AddCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validUntil":{"type":"string","format":"date"}}},"CreateRecipeRequest":{"required":["articleId","name","outputQuantity","outputUom","type"],"type":"object","properties":{"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string","enum":["RAW_MATERIAL","INTERMEDIATE","FINISHED_PRODUCT"]},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32"},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"}}},"IngredientResponse":{"required":["articleId","id","position","quantity","substitutable","uom"],"type":"object","properties":{"id":{"type":"string"},"position":{"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string","nullable":true},"substitutable":{"type":"boolean"}}},"ProductionStepResponse":{"required":["description","id","stepNumber"],"type":"object","properties":{"id":{"type":"string"},"stepNumber":{"type":"integer","format":"int32"},"description":{"type":"string"},"durationMinutes":{"type":"integer","format":"int32","nullable":true},"temperatureCelsius":{"type":"integer","format":"int32","nullable":true}}},"RecipeResponse":{"required":["articleId","createdAt","description","id","ingredients","name","outputQuantity","outputUom","productionSteps","status","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/IngredientResponse"}},"productionSteps":{"type":"array","items":{"$ref":"#/components/schemas/ProductionStepResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AddProductionStepRequest":{"required":["description"],"type":"object","properties":{"stepNumber":{"minimum":1,"type":"integer","format":"int32"},"description":{"maxLength":500,"minLength":0,"type":"string"},"durationMinutes":{"minimum":1,"type":"integer","format":"int32"},"temperatureCelsius":{"maximum":1000,"minimum":-273,"type":"integer","format":"int32"}}},"AddRecipeIngredientRequest":{"required":["articleId","quantity","uom"],"type":"object","properties":{"position":{"minimum":1,"type":"integer","format":"int32"},"articleId":{"type":"string"},"quantity":{"type":"string"},"uom":{"type":"string"},"subRecipeId":{"type":"string"},"substitutable":{"type":"boolean"}}},"CreateProductionOrderRequest":{"required":["plannedDate","plannedQuantity","plannedQuantityUnit","priority","recipeId"],"type":"object","properties":{"recipeId":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"plannedDate":{"type":"string","format":"date"},"priority":{"type":"string"},"notes":{"type":"string"}}},"ProductionOrderResponse":{"type":"object","properties":{"id":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"batchId":{"type":"string"},"batchNumber":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"plannedDate":{"type":"string","format":"date"},"priority":{"type":"string"},"notes":{"type":"string"},"cancelledReason":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"RescheduleProductionOrderRequest":{"required":["newPlannedDate"],"type":"object","properties":{"newPlannedDate":{"type":"string","format":"date"}}},"CancelProductionOrderRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"PlanBatchRequest":{"required":["bestBeforeDate","plannedQuantity","plannedQuantityUnit","productionDate","recipeId"],"type":"object","properties":{"recipeId":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"}}},"BatchResponse":{"type":"object","properties":{"id":{"type":"string"},"batchNumber":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"actualQuantity":{"type":"string"},"actualQuantityUnit":{"type":"string"},"waste":{"type":"string"},"wasteUnit":{"type":"string"},"remarks":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"},"consumptions":{"type":"array","items":{"$ref":"#/components/schemas/ConsumptionResponse"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"cancellationReason":{"type":"string"},"cancelledAt":{"type":"string","format":"date-time"}}},"ConsumptionResponse":{"type":"object","properties":{"id":{"type":"string"},"inputBatchId":{"type":"string"},"articleId":{"type":"string"},"quantityUsed":{"type":"string"},"quantityUsedUnit":{"type":"string"},"consumedAt":{"type":"string","format":"date-time"}}},"RecordConsumptionRequest":{"required":["articleId","inputBatchId","quantityUnit","quantityUsed"],"type":"object","properties":{"inputBatchId":{"type":"string"},"articleId":{"type":"string"},"quantityUsed":{"type":"string"},"quantityUnit":{"type":"string"}}},"CompleteBatchRequest":{"required":["actualQuantity","actualQuantityUnit","waste","wasteUnit"],"type":"object","properties":{"actualQuantity":{"type":"string"},"actualQuantityUnit":{"type":"string"},"waste":{"type":"string"},"wasteUnit":{"type":"string"},"remarks":{"type":"string"}}},"CancelBatchRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"CreateStorageLocationRequest":{"required":["name","storageType"],"type":"object","properties":{"name":{"type":"string"},"storageType":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"CreateStockRequest":{"required":["articleId","storageLocationId"],"type":"object","properties":{"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevelAmount":{"type":"string"},"minimumLevelUnit":{"type":"string"},"minimumShelfLifeDays":{"type":"integer","format":"int32"}}},"CreateStockResponse":{"required":["articleId","id","storageLocationId"],"type":"object","properties":{"id":{"type":"string"},"articleId":{"type":"string"},"storageLocationId":{"type":"string"},"minimumLevel":{"$ref":"#/components/schemas/MinimumLevelResponse"},"minimumShelfLifeDays":{"type":"integer","format":"int32","nullable":true}}},"ReserveStockRequest":{"required":["priority","quantityAmount","quantityUnit","referenceId","referenceType"],"type":"object","properties":{"referenceType":{"type":"string"},"referenceId":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"priority":{"type":"string"}}},"AddStockBatchRequest":{"required":["batchId","batchType","expiryDate","quantityAmount","quantityUnit"],"type":"object","properties":{"batchId":{"type":"string"},"batchType":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"expiryDate":{"type":"string"}}},"RemoveStockBatchRequest":{"required":["quantityAmount","quantityUnit"],"type":"object","properties":{"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"}}},"BlockStockBatchRequest":{"required":["reason"],"type":"object","properties":{"reason":{"type":"string"}}},"RecordStockMovementRequest":{"required":["articleId","batchId","batchType","movementType","quantityAmount","quantityUnit","stockBatchId","stockId"],"type":"object","properties":{"stockId":{"type":"string"},"articleId":{"type":"string"},"stockBatchId":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"movementType":{"type":"string"},"direction":{"type":"string"},"quantityAmount":{"type":"string"},"quantityUnit":{"type":"string"},"reason":{"type":"string"},"referenceDocumentId":{"type":"string"}}},"StockMovementResponse":{"required":["articleId","batchId","batchType","direction","id","movementType","performedAt","performedBy","quantityAmount","quantityUnit","stockBatchId","stockId"],"type":"object","properties":{"id":{"type":"string"},"stockId":{"type":"string"},"articleId":{"type":"string"},"stockBatchId":{"type":"string"},"batchId":{"type":"string"},"batchType":{"type":"string"},"movementType":{"type":"string"},"direction":{"type":"string"},"quantityAmount":{"type":"number"},"quantityUnit":{"type":"string"},"reason":{"type":"string","nullable":true},"referenceDocumentId":{"type":"string","nullable":true},"performedBy":{"type":"string"},"performedAt":{"type":"string","format":"date-time"}}},"CreateCustomerRequest":{"required":["city","country","name","phone","postalCode","street","type"],"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["B2C","B2B"]},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"contactPerson":{"type":"string"},"paymentDueDays":{"type":"integer","format":"int32"},"paymentDescription":{"type":"string"}}},"AddDeliveryAddressRequest":{"required":["city","country","postalCode","street"],"type":"object","properties":{"label":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"contactPerson":{"type":"string"},"deliveryNotes":{"type":"string"}}},"CreateProductCategoryRequest":{"required":["name"],"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}},"RefreshTokenRequest":{"required":["refreshToken"],"type":"object","properties":{"refreshToken":{"type":"string","description":"Refresh token"}},"description":"Refresh token request"},"LoginResponse":{"required":["accessToken","expiresAt","expiresIn","refreshToken","tokenType"],"type":"object","properties":{"accessToken":{"type":"string","description":"JWT access token","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"tokenType":{"type":"string","description":"Token type","example":"Bearer"},"expiresIn":{"type":"integer","description":"Token expiration time in seconds","format":"int64","example":3600},"expiresAt":{"type":"string","description":"Token expiration timestamp","format":"date-time"},"refreshToken":{"type":"string","description":"Refresh token for obtaining new access token"}},"description":"Login response with JWT tokens"},"LoginRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string","description":"Username","example":"admin"},"password":{"type":"string","description":"Password","example":"admin123"}},"description":"Login request with username and password"},"CreateArticleRequest":{"required":["articleNumber","categoryId","name","price","priceModel","unit"],"type":"object","properties":{"name":{"type":"string"},"articleNumber":{"type":"string"},"categoryId":{"type":"string"},"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"AssignSupplierRequest":{"required":["supplierId"],"type":"object","properties":{"supplierId":{"type":"string"}}},"AddSalesUnitRequest":{"required":["price","priceModel","unit"],"type":"object","properties":{"unit":{"type":"string","enum":["PIECE_FIXED","KG","HUNDRED_GRAM","PIECE_VARIABLE"]},"priceModel":{"type":"string","enum":["FIXED","WEIGHT_BASED"]},"price":{"type":"number"}}},"RecipeSummaryResponse":{"required":["articleId","createdAt","description","id","ingredientCount","name","outputQuantity","outputUom","status","stepCount","type","updatedAt","version","yieldPercentage"],"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"version":{"type":"integer","format":"int32"},"type":{"type":"string"},"description":{"type":"string"},"yieldPercentage":{"type":"integer","format":"int32"},"shelfLifeDays":{"type":"integer","format":"int32","nullable":true},"outputQuantity":{"type":"string"},"outputUom":{"type":"string"},"articleId":{"type":"string"},"status":{"type":"string"},"ingredientCount":{"type":"integer","format":"int32"},"stepCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"BatchSummaryResponse":{"type":"object","properties":{"id":{"type":"string"},"batchNumber":{"type":"string"},"recipeId":{"type":"string"},"status":{"type":"string"},"plannedQuantity":{"type":"string"},"plannedQuantityUnit":{"type":"string"},"productionDate":{"type":"string","format":"date"},"bestBeforeDate":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"CountryResponse":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"}}},"RemoveCertificateRequest":{"required":["certificateType"],"type":"object","properties":{"certificateType":{"type":"string"},"issuer":{"type":"string"},"validFrom":{"type":"string","format":"date"}}}},"securitySchemes":{"Bearer Authentication":{"type":"http","description":"JWT authentication token obtained from POST /api/auth/login.\n\nFormat: Bearer \n\nExample:\nBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\n","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file diff --git a/frontend/packages/api-client/src/index.ts b/frontend/packages/api-client/src/index.ts index 0752c98..b990c60 100644 --- a/frontend/packages/api-client/src/index.ts +++ b/frontend/packages/api-client/src/index.ts @@ -115,7 +115,6 @@ export type { ReserveStockRequest, StockMovementDTO, RecordStockMovementRequest, - StartProductionOrderRequest, CountryDTO, } from '@effigenix/types'; diff --git a/frontend/packages/api-client/src/resources/production-orders.ts b/frontend/packages/api-client/src/resources/production-orders.ts index fb86a70..1a9b91c 100644 --- a/frontend/packages/api-client/src/resources/production-orders.ts +++ b/frontend/packages/api-client/src/resources/production-orders.ts @@ -1,7 +1,7 @@ /** Production Orders resource – Production BC. */ import type { AxiosInstance } from 'axios'; -import type { ProductionOrderDTO, CreateProductionOrderRequest, StartProductionOrderRequest, RescheduleProductionOrderRequest } from '@effigenix/types'; +import type { ProductionOrderDTO, CreateProductionOrderRequest, RescheduleProductionOrderRequest } from '@effigenix/types'; export type Priority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT'; @@ -28,7 +28,7 @@ export interface ProductionOrderFilter { dateTo?: string; } -export type { ProductionOrderDTO, CreateProductionOrderRequest, StartProductionOrderRequest, RescheduleProductionOrderRequest }; +export type { ProductionOrderDTO, CreateProductionOrderRequest, RescheduleProductionOrderRequest }; const BASE = '/api/production/production-orders'; @@ -58,8 +58,8 @@ export function createProductionOrdersResource(client: AxiosInstance) { return res.data; }, - async start(id: string, request: StartProductionOrderRequest): Promise { - const res = await client.post(`${BASE}/${id}/start`, request); + async start(id: string): Promise { + const res = await client.post(`${BASE}/${id}/start`); return res.data; }, diff --git a/frontend/packages/types/src/generated/api.ts b/frontend/packages/types/src/generated/api.ts index b549a06..78e142b 100644 --- a/frontend/packages/types/src/generated/api.ts +++ b/frontend/packages/types/src/generated/api.ts @@ -1702,6 +1702,7 @@ export interface components { recipeId?: string; status?: string; batchId?: string; + batchNumber?: string; plannedQuantity?: string; plannedQuantityUnit?: string; /** Format: date */ @@ -1714,9 +1715,6 @@ export interface components { /** Format: date-time */ updatedAt?: string; }; - StartProductionOrderRequest: { - batchId: string; - }; RescheduleProductionOrderRequest: { /** Format: date */ newPlannedDate: string; @@ -3118,11 +3116,7 @@ export interface operations { }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["StartProductionOrderRequest"]; - }; - }; + requestBody?: never; responses: { /** @description OK */ 200: { diff --git a/frontend/packages/types/src/production.ts b/frontend/packages/types/src/production.ts index ca76810..e9338da 100644 --- a/frontend/packages/types/src/production.ts +++ b/frontend/packages/types/src/production.ts @@ -30,6 +30,5 @@ export type CancelBatchRequest = components['schemas']['CancelBatchRequest']; // Production Order types export type ProductionOrderDTO = components['schemas']['ProductionOrderResponse']; export type CreateProductionOrderRequest = components['schemas']['CreateProductionOrderRequest']; -export type StartProductionOrderRequest = components['schemas']['StartProductionOrderRequest']; export type RescheduleProductionOrderRequest = components['schemas']['RescheduleProductionOrderRequest']; export type CancelProductionOrderRequest = components['schemas']['CancelProductionOrderRequest'];