diff --git a/backend/src/main/java/de/effigenix/application/inventory/CreateStock.java b/backend/src/main/java/de/effigenix/application/inventory/CreateStock.java new file mode 100644 index 0000000..e46bc96 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/inventory/CreateStock.java @@ -0,0 +1,59 @@ +package de.effigenix.application.inventory; + +import de.effigenix.application.inventory.command.CreateStockCommand; +import de.effigenix.domain.inventory.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public class CreateStock { + + private final StockRepository stockRepository; + + public CreateStock(StockRepository stockRepository) { + this.stockRepository = stockRepository; + } + + public Result execute(CreateStockCommand cmd) { + // 1. Draft aus Command bauen (kein VO-Wissen im Use Case) + var draft = new StockDraft( + cmd.articleId(), cmd.storageLocationId(), + cmd.minimumLevelAmount(), cmd.minimumLevelUnit(), + cmd.minimumShelfLifeDays() + ); + + // 2. Aggregate erzeugen (validiert intern) + Stock stock; + switch (Stock.create(draft)) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var val) -> stock = val; + } + + // 3. Uniqueness-Check (Application-Concern: braucht Repository) + switch (stockRepository.existsByArticleIdAndStorageLocationId(stock.articleId(), stock.storageLocationId())) { + case Result.Failure(var err) -> + { return Result.failure(new StockError.RepositoryFailure(err.message())); } + case Result.Success(var exists) -> { + if (exists) { + return Result.failure(new StockError.DuplicateStock( + cmd.articleId(), cmd.storageLocationId())); + } + } + } + + // 4. Speichern (Race-Condition-Schutz: DuplicateEntry → DuplicateStock) + switch (stockRepository.save(stock)) { + case Result.Failure(var err) -> { + if (err instanceof RepositoryError.DuplicateEntry) { + return Result.failure(new StockError.DuplicateStock( + cmd.articleId(), cmd.storageLocationId())); + } + return Result.failure(new StockError.RepositoryFailure(err.message())); + } + case Result.Success(var ignored) -> { } + } + + return Result.success(stock); + } +} diff --git a/backend/src/main/java/de/effigenix/application/inventory/command/CreateStockCommand.java b/backend/src/main/java/de/effigenix/application/inventory/command/CreateStockCommand.java new file mode 100644 index 0000000..9500d31 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/inventory/command/CreateStockCommand.java @@ -0,0 +1,9 @@ +package de.effigenix.application.inventory.command; + +public record CreateStockCommand( + String articleId, + String storageLocationId, + String minimumLevelAmount, + String minimumLevelUnit, + Integer minimumShelfLifeDays +) {} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/MinimumLevel.java b/backend/src/main/java/de/effigenix/domain/inventory/MinimumLevel.java new file mode 100644 index 0000000..f96ba63 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/inventory/MinimumLevel.java @@ -0,0 +1,47 @@ +package de.effigenix.domain.inventory; + +import de.effigenix.shared.common.Quantity; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.UnitOfMeasure; + +import java.math.BigDecimal; + +public record MinimumLevel(Quantity quantity) { + + public static Result of(String amount, String unit) { + if (amount == null || amount.isBlank()) { + return Result.failure(new StockError.InvalidMinimumLevel("amount must not be blank")); + } + if (unit == null || unit.isBlank()) { + return Result.failure(new StockError.InvalidMinimumLevel("unit must not be blank")); + } + + BigDecimal parsedAmount; + try { + parsedAmount = new BigDecimal(amount); + } catch (NumberFormatException e) { + return Result.failure(new StockError.InvalidMinimumLevel("amount is not a valid number: " + amount)); + } + + if (parsedAmount.compareTo(BigDecimal.ZERO) < 0) { + return Result.failure(new StockError.InvalidMinimumLevel("amount must be >= 0, was: " + amount)); + } + + UnitOfMeasure parsedUnit; + try { + parsedUnit = UnitOfMeasure.valueOf(unit); + } catch (IllegalArgumentException e) { + return Result.failure(new StockError.InvalidMinimumLevel("invalid unit: " + unit)); + } + + // MinimumLevel erlaubt amount == 0 (kein Mindestbestand) + // Quantity.of() verlangt amount > 0, daher Reconstitute verwenden + if (parsedAmount.compareTo(BigDecimal.ZERO) == 0) { + return Result.success(new MinimumLevel(Quantity.reconstitute(parsedAmount, parsedUnit, null, null))); + } + + return Quantity.of(parsedAmount, parsedUnit) + .map(MinimumLevel::new) + .mapError(qErr -> new StockError.InvalidMinimumLevel(qErr.message())); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/MinimumShelfLife.java b/backend/src/main/java/de/effigenix/domain/inventory/MinimumShelfLife.java new file mode 100644 index 0000000..60e9db1 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/inventory/MinimumShelfLife.java @@ -0,0 +1,13 @@ +package de.effigenix.domain.inventory; + +import de.effigenix.shared.common.Result; + +public record MinimumShelfLife(int days) { + + public static Result of(Integer days) { + if (days == null || days <= 0) { + return Result.failure(new StockError.InvalidMinimumShelfLife(days == null ? 0 : days)); + } + return Result.success(new MinimumShelfLife(days)); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/Stock.java b/backend/src/main/java/de/effigenix/domain/inventory/Stock.java new file mode 100644 index 0000000..d20fcf0 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/inventory/Stock.java @@ -0,0 +1,125 @@ +package de.effigenix.domain.inventory; + +import de.effigenix.domain.masterdata.ArticleId; +import de.effigenix.shared.common.Result; + +import java.util.Objects; + +/** + * Stock aggregate root. + * + * Invariants: + * - ArticleId + StorageLocationId = unique (Application Layer, Repository-Concern) + * - MinimumLevel optional (quantity amount >= 0) + * - MinimumShelfLife optional (days > 0) + */ +public class Stock { + + private final StockId id; + private final ArticleId articleId; + private final StorageLocationId storageLocationId; + private MinimumLevel minimumLevel; + private MinimumShelfLife minimumShelfLife; + + private Stock( + StockId id, + ArticleId articleId, + StorageLocationId storageLocationId, + MinimumLevel minimumLevel, + MinimumShelfLife minimumShelfLife + ) { + this.id = id; + this.articleId = articleId; + this.storageLocationId = storageLocationId; + this.minimumLevel = minimumLevel; + this.minimumShelfLife = minimumShelfLife; + } + + /** + * Factory: Erzeugt ein neues Stock-Aggregat aus rohen Eingaben. + * Orchestriert Validierung aller VOs intern. + */ + public static Result create(StockDraft draft) { + // 1. ArticleId validieren (Pflicht) + if (draft.articleId() == null || draft.articleId().isBlank()) { + return Result.failure(new StockError.InvalidArticleId("must not be blank")); + } + ArticleId articleId; + try { + articleId = ArticleId.of(draft.articleId()); + } catch (IllegalArgumentException e) { + return Result.failure(new StockError.InvalidArticleId(e.getMessage())); + } + + // 2. StorageLocationId validieren (Pflicht) + if (draft.storageLocationId() == null || draft.storageLocationId().isBlank()) { + return Result.failure(new StockError.InvalidStorageLocationId("must not be blank")); + } + StorageLocationId storageLocationId; + try { + storageLocationId = StorageLocationId.of(draft.storageLocationId()); + } catch (IllegalArgumentException e) { + return Result.failure(new StockError.InvalidStorageLocationId(e.getMessage())); + } + + // 3. MinimumLevel optional + MinimumLevel minimumLevel = null; + if (draft.minimumLevelAmount() != null || draft.minimumLevelUnit() != null) { + switch (MinimumLevel.of(draft.minimumLevelAmount(), draft.minimumLevelUnit())) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var val) -> minimumLevel = val; + } + } + + // 4. MinimumShelfLife optional + MinimumShelfLife minimumShelfLife = null; + if (draft.minimumShelfLifeDays() != null) { + switch (MinimumShelfLife.of(draft.minimumShelfLifeDays())) { + case Result.Failure(var err) -> { return Result.failure(err); } + case Result.Success(var val) -> minimumShelfLife = val; + } + } + + return Result.success(new Stock( + StockId.generate(), articleId, storageLocationId, minimumLevel, minimumShelfLife + )); + } + + /** + * Reconstitute from persistence without re-validation. + */ + public static Stock reconstitute( + StockId id, + ArticleId articleId, + StorageLocationId storageLocationId, + MinimumLevel minimumLevel, + MinimumShelfLife minimumShelfLife + ) { + return new Stock(id, articleId, storageLocationId, minimumLevel, minimumShelfLife); + } + + // ==================== Getters ==================== + + public StockId id() { return id; } + public ArticleId articleId() { return articleId; } + public StorageLocationId storageLocationId() { return storageLocationId; } + public MinimumLevel minimumLevel() { return minimumLevel; } + public MinimumShelfLife minimumShelfLife() { return minimumShelfLife; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Stock other)) return false; + return Objects.equals(id, other.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Stock{id=" + id + ", articleId=" + articleId + ", storageLocationId=" + storageLocationId + "}"; + } +} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StockDraft.java b/backend/src/main/java/de/effigenix/domain/inventory/StockDraft.java new file mode 100644 index 0000000..23be3f6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/inventory/StockDraft.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.inventory; + +/** + * Rohe Eingabe zum Erzeugen eines Stock-Aggregates. + * Wird vom Application Layer aus dem Command gebaut und an Stock.create() übergeben. + * Das Aggregate ist verantwortlich für Validierung und VO-Konstruktion. + * + * @param articleId Pflicht + * @param storageLocationId Pflicht + * @param minimumLevelAmount Optional – BigDecimal als String, nullable + * @param minimumLevelUnit Optional – UnitOfMeasure als String, nullable + * @param minimumShelfLifeDays Optional – Integer, nullable + */ +public record StockDraft( + String articleId, + String storageLocationId, + String minimumLevelAmount, + String minimumLevelUnit, + Integer minimumShelfLifeDays +) {} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StockError.java b/backend/src/main/java/de/effigenix/domain/inventory/StockError.java new file mode 100644 index 0000000..8f924c8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/inventory/StockError.java @@ -0,0 +1,45 @@ +package de.effigenix.domain.inventory; + +public sealed interface StockError { + + String code(); + String message(); + + record DuplicateStock(String articleId, String storageLocationId) implements StockError { + @Override public String code() { return "DUPLICATE_STOCK"; } + @Override public String message() { return "Stock already exists for article " + articleId + " at location " + storageLocationId; } + } + + record InvalidMinimumLevel(String reason) implements StockError { + @Override public String code() { return "INVALID_MINIMUM_LEVEL"; } + @Override public String message() { return "Invalid minimum level: " + reason; } + } + + record InvalidMinimumShelfLife(int days) implements StockError { + @Override public String code() { return "INVALID_MINIMUM_SHELF_LIFE"; } + @Override public String message() { return "Minimum shelf life must be > 0, was: " + days; } + } + + record StockNotFound(String id) implements StockError { + @Override public String code() { return "STOCK_NOT_FOUND"; } + @Override public String message() { return "Stock not found: " + id; } + } + + record InvalidArticleId(String reason) implements StockError { + @Override public String code() { return "INVALID_ARTICLE_ID"; } + @Override public String message() { return "Invalid article ID: " + reason; } + } + + record InvalidStorageLocationId(String reason) implements StockError { + @Override public String code() { return "INVALID_STORAGE_LOCATION_ID"; } + @Override public String message() { return "Invalid storage location ID: " + reason; } + } + + record Unauthorized(String message) implements StockError { + @Override public String code() { return "UNAUTHORIZED"; } + } + + record RepositoryFailure(String message) implements StockError { + @Override public String code() { return "REPOSITORY_ERROR"; } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StockId.java b/backend/src/main/java/de/effigenix/domain/inventory/StockId.java new file mode 100644 index 0000000..7fb017e --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/inventory/StockId.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.inventory; + +import java.util.UUID; + +public record StockId(String value) { + + public StockId { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("StockId must not be blank"); + } + } + + public static StockId generate() { + return new StockId(UUID.randomUUID().toString()); + } + + public static StockId of(String value) { + return new StockId(value); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/inventory/StockRepository.java b/backend/src/main/java/de/effigenix/domain/inventory/StockRepository.java new file mode 100644 index 0000000..7d848b0 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/inventory/StockRepository.java @@ -0,0 +1,18 @@ +package de.effigenix.domain.inventory; + +import de.effigenix.domain.masterdata.ArticleId; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; + +import java.util.Optional; + +public interface StockRepository { + + Result> findById(StockId id); + + Result> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId); + + Result existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId); + + Result save(Stock stock); +} diff --git a/backend/src/main/java/de/effigenix/domain/production/BatchNumber.java b/backend/src/main/java/de/effigenix/domain/production/BatchNumber.java index 9ea6040..3e9ee72 100644 --- a/backend/src/main/java/de/effigenix/domain/production/BatchNumber.java +++ b/backend/src/main/java/de/effigenix/domain/production/BatchNumber.java @@ -1,5 +1,6 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.QuantityError; import de.effigenix.shared.common.Result; import java.time.LocalDate; diff --git a/backend/src/main/java/de/effigenix/domain/production/Ingredient.java b/backend/src/main/java/de/effigenix/domain/production/Ingredient.java index c6fb478..a431ed2 100644 --- a/backend/src/main/java/de/effigenix/domain/production/Ingredient.java +++ b/backend/src/main/java/de/effigenix/domain/production/Ingredient.java @@ -1,6 +1,8 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.UnitOfMeasure; import java.math.BigDecimal; 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 a3cd9cb..c1fc2fb 100644 --- a/backend/src/main/java/de/effigenix/domain/production/Recipe.java +++ b/backend/src/main/java/de/effigenix/domain/production/Recipe.java @@ -1,6 +1,8 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Result; +import de.effigenix.shared.common.UnitOfMeasure; import java.math.BigDecimal; import java.time.LocalDateTime; diff --git a/backend/src/main/java/de/effigenix/domain/production/YieldPercentage.java b/backend/src/main/java/de/effigenix/domain/production/YieldPercentage.java index 7210d6a..eb51287 100644 --- a/backend/src/main/java/de/effigenix/domain/production/YieldPercentage.java +++ b/backend/src/main/java/de/effigenix/domain/production/YieldPercentage.java @@ -1,5 +1,7 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.Quantity; +import de.effigenix.shared.common.QuantityError; import de.effigenix.shared.common.Result; import java.math.BigDecimal; diff --git a/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java b/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java index 3845f77..734ea5d 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java +++ b/backend/src/main/java/de/effigenix/infrastructure/config/InventoryUseCaseConfiguration.java @@ -1,10 +1,12 @@ package de.effigenix.infrastructure.config; import de.effigenix.application.inventory.ActivateStorageLocation; +import de.effigenix.application.inventory.CreateStock; import de.effigenix.application.inventory.CreateStorageLocation; import de.effigenix.application.inventory.DeactivateStorageLocation; import de.effigenix.application.inventory.ListStorageLocations; import de.effigenix.application.inventory.UpdateStorageLocation; +import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StorageLocationRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,4 +40,11 @@ public class InventoryUseCaseConfiguration { public ListStorageLocations listStorageLocations(StorageLocationRepository storageLocationRepository) { return new ListStorageLocations(storageLocationRepository); } + + // ==================== Stock Use Cases ==================== + + @Bean + public CreateStock createStock(StockRepository stockRepository) { + return new CreateStock(stockRepository); + } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockEntity.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockEntity.java new file mode 100644 index 0000000..460aa73 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/entity/StockEntity.java @@ -0,0 +1,51 @@ +package de.effigenix.infrastructure.inventory.persistence.entity; + +import jakarta.persistence.*; + +import java.math.BigDecimal; + +@Entity +@Table(name = "stocks") +public class StockEntity { + + @Id + @Column(name = "id", nullable = false, length = 36) + private String id; + + @Column(name = "article_id", nullable = false, length = 36) + private String articleId; + + @Column(name = "storage_location_id", nullable = false, length = 36) + private String storageLocationId; + + @Column(name = "minimum_level_amount", precision = 12, scale = 3) + private BigDecimal minimumLevelAmount; + + @Column(name = "minimum_level_unit", length = 20) + private String minimumLevelUnit; + + @Column(name = "minimum_shelf_life_days") + private Integer minimumShelfLifeDays; + + public StockEntity() {} + + // ==================== Getters & Setters ==================== + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + + public String getArticleId() { return articleId; } + public void setArticleId(String articleId) { this.articleId = articleId; } + + public String getStorageLocationId() { return storageLocationId; } + public void setStorageLocationId(String storageLocationId) { this.storageLocationId = storageLocationId; } + + public BigDecimal getMinimumLevelAmount() { return minimumLevelAmount; } + public void setMinimumLevelAmount(BigDecimal minimumLevelAmount) { this.minimumLevelAmount = minimumLevelAmount; } + + public String getMinimumLevelUnit() { return minimumLevelUnit; } + public void setMinimumLevelUnit(String minimumLevelUnit) { this.minimumLevelUnit = minimumLevelUnit; } + + public Integer getMinimumShelfLifeDays() { return minimumShelfLifeDays; } + public void setMinimumShelfLifeDays(Integer minimumShelfLifeDays) { this.minimumShelfLifeDays = minimumShelfLifeDays; } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMapper.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMapper.java new file mode 100644 index 0000000..6fd94ea --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/mapper/StockMapper.java @@ -0,0 +1,55 @@ +package de.effigenix.infrastructure.inventory.persistence.mapper; + +import de.effigenix.domain.inventory.*; +import de.effigenix.domain.masterdata.ArticleId; +import de.effigenix.shared.common.Quantity; +import de.effigenix.shared.common.UnitOfMeasure; +import de.effigenix.infrastructure.inventory.persistence.entity.StockEntity; +import org.springframework.stereotype.Component; + +@Component +public class StockMapper { + + public StockEntity toEntity(Stock stock) { + var entity = new StockEntity(); + entity.setId(stock.id().value()); + entity.setArticleId(stock.articleId().value()); + entity.setStorageLocationId(stock.storageLocationId().value()); + + if (stock.minimumLevel() != null) { + entity.setMinimumLevelAmount(stock.minimumLevel().quantity().amount()); + entity.setMinimumLevelUnit(stock.minimumLevel().quantity().uom().name()); + } + + if (stock.minimumShelfLife() != null) { + entity.setMinimumShelfLifeDays(stock.minimumShelfLife().days()); + } + + return entity; + } + + public Stock toDomain(StockEntity entity) { + MinimumLevel minimumLevel = null; + if (entity.getMinimumLevelAmount() != null && entity.getMinimumLevelUnit() != null) { + var quantity = Quantity.reconstitute( + entity.getMinimumLevelAmount(), + UnitOfMeasure.valueOf(entity.getMinimumLevelUnit()), + null, null + ); + minimumLevel = new MinimumLevel(quantity); + } + + MinimumShelfLife minimumShelfLife = null; + if (entity.getMinimumShelfLifeDays() != null) { + minimumShelfLife = new MinimumShelfLife(entity.getMinimumShelfLifeDays()); + } + + return Stock.reconstitute( + StockId.of(entity.getId()), + ArticleId.of(entity.getArticleId()), + StorageLocationId.of(entity.getStorageLocationId()), + minimumLevel, + minimumShelfLife + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockRepository.java new file mode 100644 index 0000000..9d82a98 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/JpaStockRepository.java @@ -0,0 +1,82 @@ +package de.effigenix.infrastructure.inventory.persistence.repository; + +import de.effigenix.domain.inventory.*; +import de.effigenix.domain.masterdata.ArticleId; +import de.effigenix.infrastructure.inventory.persistence.mapper.StockMapper; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Repository +@Profile("!no-db") +@Transactional(readOnly = true) +public class JpaStockRepository implements StockRepository { + + private static final Logger logger = LoggerFactory.getLogger(JpaStockRepository.class); + + private final StockJpaRepository jpaRepository; + private final StockMapper mapper; + + public JpaStockRepository(StockJpaRepository jpaRepository, StockMapper mapper) { + this.jpaRepository = jpaRepository; + this.mapper = mapper; + } + + @Override + public Result> findById(StockId id) { + try { + Optional result = jpaRepository.findById(id.value()) + .map(mapper::toDomain); + return Result.success(result); + } catch (Exception e) { + logger.trace("Database error in findById", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { + try { + Optional result = jpaRepository.findByArticleIdAndStorageLocationId(articleId.value(), storageLocationId.value()) + .map(mapper::toDomain); + return Result.success(result); + } catch (Exception e) { + logger.trace("Database error in findByArticleIdAndStorageLocationId", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { + try { + return Result.success(jpaRepository.existsByArticleIdAndStorageLocationId(articleId.value(), storageLocationId.value())); + } catch (Exception e) { + logger.trace("Database error in existsByArticleIdAndStorageLocationId", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + @Transactional + public Result save(Stock stock) { + try { + jpaRepository.save(mapper.toEntity(stock)); + return Result.success(null); + } catch (DataIntegrityViolationException e) { + logger.trace("Duplicate entry in save", e); + return Result.failure(new RepositoryError.DuplicateEntry( + "Stock already exists for article " + stock.articleId().value() + + " at location " + stock.storageLocationId().value())); + } catch (Exception e) { + logger.trace("Database error in save", e); + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockJpaRepository.java new file mode 100644 index 0000000..287bab5 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/persistence/repository/StockJpaRepository.java @@ -0,0 +1,13 @@ +package de.effigenix.infrastructure.inventory.persistence.repository; + +import de.effigenix.infrastructure.inventory.persistence.entity.StockEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface StockJpaRepository extends JpaRepository { + + Optional findByArticleIdAndStorageLocationId(String articleId, String storageLocationId); + + boolean existsByArticleIdAndStorageLocationId(String articleId, String storageLocationId); +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockController.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockController.java new file mode 100644 index 0000000..6000c7b --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/controller/StockController.java @@ -0,0 +1,70 @@ +package de.effigenix.infrastructure.inventory.web.controller; + +import de.effigenix.application.inventory.CreateStock; +import de.effigenix.application.inventory.command.CreateStockCommand; +import de.effigenix.domain.inventory.StockError; +import de.effigenix.infrastructure.inventory.web.dto.CreateStockRequest; +import de.effigenix.infrastructure.inventory.web.dto.StockResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/inventory/stocks") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Stocks", description = "Stock management endpoints") +public class StockController { + + private static final Logger logger = LoggerFactory.getLogger(StockController.class); + + private final CreateStock createStock; + + public StockController(CreateStock createStock) { + this.createStock = createStock; + } + + @PostMapping + @PreAuthorize("hasAuthority('STOCK_WRITE')") + public ResponseEntity createStock( + @Valid @RequestBody CreateStockRequest request, + Authentication authentication + ) { + logger.info("Creating stock for article: {} at location: {} by actor: {}", + request.articleId(), request.storageLocationId(), authentication.getName()); + + var cmd = new CreateStockCommand( + request.articleId(), request.storageLocationId(), + request.minimumLevelAmount(), request.minimumLevelUnit(), + request.minimumShelfLifeDays() + ); + var result = createStock.execute(cmd); + + if (result.isFailure()) { + throw new StockDomainErrorException(result.unsafeGetError()); + } + + logger.info("Stock created: {}", result.unsafeGetValue().id().value()); + return ResponseEntity.status(HttpStatus.CREATED) + .body(StockResponse.from(result.unsafeGetValue())); + } + + public static class StockDomainErrorException extends RuntimeException { + private final StockError error; + + public StockDomainErrorException(StockError error) { + super(error.message()); + this.error = error; + } + + public StockError getError() { + return error; + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/CreateStockRequest.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/CreateStockRequest.java new file mode 100644 index 0000000..4b6f6a7 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/CreateStockRequest.java @@ -0,0 +1,11 @@ +package de.effigenix.infrastructure.inventory.web.dto; + +import jakarta.validation.constraints.NotBlank; + +public record CreateStockRequest( + @NotBlank String articleId, + @NotBlank String storageLocationId, + String minimumLevelAmount, + String minimumLevelUnit, + Integer minimumShelfLifeDays +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/StockResponse.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/StockResponse.java new file mode 100644 index 0000000..485d570 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/dto/StockResponse.java @@ -0,0 +1,42 @@ +package de.effigenix.infrastructure.inventory.web.dto; + +import de.effigenix.domain.inventory.Stock; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.math.BigDecimal; + +@Schema(requiredProperties = {"id", "articleId", "storageLocationId"}) +public record StockResponse( + String id, + String articleId, + String storageLocationId, + @Schema(nullable = true) MinimumLevelResponse minimumLevel, + @Schema(nullable = true) Integer minimumShelfLifeDays +) { + + public static StockResponse from(Stock stock) { + MinimumLevelResponse minimumLevel = null; + if (stock.minimumLevel() != null) { + minimumLevel = new MinimumLevelResponse( + stock.minimumLevel().quantity().amount(), + stock.minimumLevel().quantity().uom().name() + ); + } + + Integer shelfLifeDays = null; + if (stock.minimumShelfLife() != null) { + shelfLifeDays = stock.minimumShelfLife().days(); + } + + return new StockResponse( + stock.id().value(), + stock.articleId().value(), + stock.storageLocationId().value(), + minimumLevel, + shelfLifeDays + ); + } + + @Schema(requiredProperties = {"amount", "unit"}) + public record MinimumLevelResponse(BigDecimal amount, String unit) {} +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/exception/InventoryErrorHttpStatusMapper.java b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/exception/InventoryErrorHttpStatusMapper.java index 9c8d4c0..c1bcf6a 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/inventory/web/exception/InventoryErrorHttpStatusMapper.java +++ b/backend/src/main/java/de/effigenix/infrastructure/inventory/web/exception/InventoryErrorHttpStatusMapper.java @@ -1,6 +1,7 @@ package de.effigenix.infrastructure.inventory.web.exception; import de.effigenix.domain.inventory.StorageLocationError; +import de.effigenix.domain.inventory.StockError; public final class InventoryErrorHttpStatusMapper { @@ -20,4 +21,17 @@ public final class InventoryErrorHttpStatusMapper { case StorageLocationError.RepositoryFailure e -> 500; }; } + + public static int toHttpStatus(StockError error) { + return switch (error) { + case StockError.StockNotFound e -> 404; + case StockError.DuplicateStock e -> 409; + case StockError.InvalidMinimumLevel e -> 400; + case StockError.InvalidMinimumShelfLife e -> 400; + case StockError.InvalidArticleId e -> 400; + case StockError.InvalidStorageLocationId e -> 400; + case StockError.Unauthorized e -> 403; + case StockError.RepositoryFailure e -> 500; + }; + } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/mapper/RecipeMapper.java b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/mapper/RecipeMapper.java index 016b828..99ebdde 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/production/persistence/mapper/RecipeMapper.java +++ b/backend/src/main/java/de/effigenix/infrastructure/production/persistence/mapper/RecipeMapper.java @@ -4,6 +4,8 @@ import de.effigenix.domain.production.*; import de.effigenix.infrastructure.production.persistence.entity.IngredientEntity; import de.effigenix.infrastructure.production.persistence.entity.ProductionStepEntity; import de.effigenix.infrastructure.production.persistence.entity.RecipeEntity; +import de.effigenix.shared.common.Quantity; +import de.effigenix.shared.common.UnitOfMeasure; import org.springframework.stereotype.Component; import java.util.List; diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java index 2c2bfa6..8bfd7ee 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package de.effigenix.infrastructure.usermanagement.web.exception; import de.effigenix.domain.inventory.StorageLocationError; +import de.effigenix.domain.inventory.StockError; import de.effigenix.domain.masterdata.ArticleError; import de.effigenix.domain.masterdata.ProductCategoryError; import de.effigenix.domain.masterdata.CustomerError; @@ -8,6 +9,7 @@ import de.effigenix.domain.masterdata.SupplierError; import de.effigenix.domain.production.RecipeError; import de.effigenix.domain.usermanagement.UserError; import de.effigenix.infrastructure.inventory.web.controller.StorageLocationController; +import de.effigenix.infrastructure.inventory.web.controller.StockController; import de.effigenix.infrastructure.inventory.web.exception.InventoryErrorHttpStatusMapper; import de.effigenix.infrastructure.masterdata.web.controller.ArticleController; import de.effigenix.infrastructure.masterdata.web.controller.CustomerController; @@ -204,6 +206,29 @@ public class GlobalExceptionHandler { return ResponseEntity.status(status).body(errorResponse); } + @ExceptionHandler(StockController.StockDomainErrorException.class) + public ResponseEntity handleStockDomainError( + StockController.StockDomainErrorException ex, + HttpServletRequest request + ) { + StockError error = ex.getError(); + int status = InventoryErrorHttpStatusMapper.toHttpStatus(error); + logDomainError("Stock", error.code(), error.message(), status); + + String clientMessage = status >= 500 + ? "An internal error occurred" + : error.message(); + + ErrorResponse errorResponse = ErrorResponse.from( + error.code(), + clientMessage, + status, + request.getRequestURI() + ); + + return ResponseEntity.status(status).body(errorResponse); + } + @ExceptionHandler(RecipeController.RecipeDomainErrorException.class) public ResponseEntity handleRecipeDomainError( RecipeController.RecipeDomainErrorException ex, diff --git a/backend/src/main/java/de/effigenix/domain/production/Quantity.java b/backend/src/main/java/de/effigenix/shared/common/Quantity.java similarity index 97% rename from backend/src/main/java/de/effigenix/domain/production/Quantity.java rename to backend/src/main/java/de/effigenix/shared/common/Quantity.java index 48622fc..cf78162 100644 --- a/backend/src/main/java/de/effigenix/domain/production/Quantity.java +++ b/backend/src/main/java/de/effigenix/shared/common/Quantity.java @@ -1,13 +1,11 @@ -package de.effigenix.domain.production; - -import de.effigenix.shared.common.Result; +package de.effigenix.shared.common; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Objects; /** - * Value Object representing a production quantity with optional Catch-Weight (dual quantity). + * Value Object representing a quantity with optional Catch-Weight (dual quantity). * *

Primary quantity is always required (amount + unit of measure). * Secondary quantity (catch-weight) is optional and used when goods are counted diff --git a/backend/src/main/java/de/effigenix/domain/production/QuantityError.java b/backend/src/main/java/de/effigenix/shared/common/QuantityError.java similarity index 97% rename from backend/src/main/java/de/effigenix/domain/production/QuantityError.java rename to backend/src/main/java/de/effigenix/shared/common/QuantityError.java index 13b496b..7f23d0c 100644 --- a/backend/src/main/java/de/effigenix/domain/production/QuantityError.java +++ b/backend/src/main/java/de/effigenix/shared/common/QuantityError.java @@ -1,4 +1,4 @@ -package de.effigenix.domain.production; +package de.effigenix.shared.common; /** * Domain errors for Quantity-related Value Objects. diff --git a/backend/src/main/java/de/effigenix/shared/common/RepositoryError.java b/backend/src/main/java/de/effigenix/shared/common/RepositoryError.java index b186ba5..7884b45 100644 --- a/backend/src/main/java/de/effigenix/shared/common/RepositoryError.java +++ b/backend/src/main/java/de/effigenix/shared/common/RepositoryError.java @@ -16,6 +16,9 @@ public sealed interface RepositoryError { } } + record DuplicateEntry(String message) implements RepositoryError { + } + record DatabaseError(String message) implements RepositoryError { } } diff --git a/backend/src/main/java/de/effigenix/domain/production/UnitOfMeasure.java b/backend/src/main/java/de/effigenix/shared/common/UnitOfMeasure.java similarity index 81% rename from backend/src/main/java/de/effigenix/domain/production/UnitOfMeasure.java rename to backend/src/main/java/de/effigenix/shared/common/UnitOfMeasure.java index 0d8e5c0..08b8513 100644 --- a/backend/src/main/java/de/effigenix/domain/production/UnitOfMeasure.java +++ b/backend/src/main/java/de/effigenix/shared/common/UnitOfMeasure.java @@ -1,7 +1,7 @@ -package de.effigenix.domain.production; +package de.effigenix.shared.common; /** - * Unit of Measure for production quantities. + * Unit of Measure for quantities. * Supports mass, volume, length, and piece-based units. */ public enum UnitOfMeasure { diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 85bac4d..f36cc70 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -6,10 +6,8 @@ springdoc: logging: level: - root: WARN + root: INFO de.effigenix: INFO - org.springframework.security: WARN - org.hibernate.SQL: WARN server: error: diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index f21f6cf..7bd1f4f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -44,8 +44,6 @@ logging: level: root: INFO de.effigenix: DEBUG - org.springframework.security: DEBUG - org.hibernate.SQL: DEBUG # CORS Configuration effigenix: diff --git a/backend/src/main/resources/db/changelog/changes/013-create-stock-schema.xml b/backend/src/main/resources/db/changelog/changes/013-create-stock-schema.xml new file mode 100644 index 0000000..808efad --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/013-create-stock-schema.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.xml b/backend/src/main/resources/db/changelog/db.changelog-master.xml index 5c78ec1..ba7d26b 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.xml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.xml @@ -17,5 +17,6 @@ + diff --git a/backend/src/test/java/de/effigenix/domain/inventory/StockTest.java b/backend/src/test/java/de/effigenix/domain/inventory/StockTest.java new file mode 100644 index 0000000..d0c5da3 --- /dev/null +++ b/backend/src/test/java/de/effigenix/domain/inventory/StockTest.java @@ -0,0 +1,262 @@ +package de.effigenix.domain.inventory; + +import de.effigenix.domain.masterdata.ArticleId; +import de.effigenix.shared.common.Quantity; +import de.effigenix.shared.common.UnitOfMeasure; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; + +class StockTest { + + // ==================== Create ==================== + + @Nested + @DisplayName("create()") + class Create { + + @Test + @DisplayName("should create Stock with all fields") + void shouldCreateWithAllFields() { + var draft = new StockDraft( + "article-1", "location-1", "10.5", "KILOGRAM", 30); + + var result = Stock.create(draft); + + assertThat(result.isSuccess()).isTrue(); + var stock = result.unsafeGetValue(); + assertThat(stock.id()).isNotNull(); + assertThat(stock.articleId().value()).isEqualTo("article-1"); + assertThat(stock.storageLocationId().value()).isEqualTo("location-1"); + assertThat(stock.minimumLevel()).isNotNull(); + assertThat(stock.minimumLevel().quantity().amount()).isEqualByComparingTo(new BigDecimal("10.5")); + assertThat(stock.minimumLevel().quantity().uom()).isEqualTo(UnitOfMeasure.KILOGRAM); + assertThat(stock.minimumShelfLife()).isNotNull(); + assertThat(stock.minimumShelfLife().days()).isEqualTo(30); + } + + @Test + @DisplayName("should create Stock with only required fields") + void shouldCreateWithOnlyRequiredFields() { + var draft = new StockDraft("article-1", "location-1", null, null, null); + + var result = Stock.create(draft); + + assertThat(result.isSuccess()).isTrue(); + var stock = result.unsafeGetValue(); + assertThat(stock.articleId().value()).isEqualTo("article-1"); + assertThat(stock.storageLocationId().value()).isEqualTo("location-1"); + assertThat(stock.minimumLevel()).isNull(); + assertThat(stock.minimumShelfLife()).isNull(); + } + + @Test + @DisplayName("should fail when articleId is null") + void shouldFailWhenArticleIdNull() { + var draft = new StockDraft(null, "location-1", null, null, null); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidArticleId.class); + } + + @Test + @DisplayName("should fail when articleId is blank") + void shouldFailWhenArticleIdBlank() { + var draft = new StockDraft("", "location-1", null, null, null); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidArticleId.class); + } + + @Test + @DisplayName("should fail when storageLocationId is null") + void shouldFailWhenStorageLocationIdNull() { + var draft = new StockDraft("article-1", null, null, null, null); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidStorageLocationId.class); + } + + @Test + @DisplayName("should fail when storageLocationId is blank") + void shouldFailWhenStorageLocationIdBlank() { + var draft = new StockDraft("article-1", "", null, null, null); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidStorageLocationId.class); + } + + @Test + @DisplayName("should fail when minimumLevel amount is negative") + void shouldFailWhenMinimumLevelNegative() { + var draft = new StockDraft("article-1", "location-1", "-1", "KILOGRAM", null); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidMinimumLevel.class); + } + + @Test + @DisplayName("should fail when minimumLevel amount is not a number") + void shouldFailWhenMinimumLevelNotNumber() { + var draft = new StockDraft("article-1", "location-1", "abc", "KILOGRAM", null); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidMinimumLevel.class); + } + + @Test + @DisplayName("should fail when minimumLevel unit is invalid") + void shouldFailWhenMinimumLevelUnitInvalid() { + var draft = new StockDraft("article-1", "location-1", "10", "INVALID_UNIT", null); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidMinimumLevel.class); + } + + @Test + @DisplayName("should accept minimumLevel amount of zero") + void shouldAcceptMinimumLevelZero() { + var draft = new StockDraft("article-1", "location-1", "0", "KILOGRAM", null); + + var result = Stock.create(draft); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.unsafeGetValue().minimumLevel().quantity().amount()) + .isEqualByComparingTo(BigDecimal.ZERO); + } + + @Test + @DisplayName("should fail when minimumShelfLife is zero") + void shouldFailWhenMinimumShelfLifeZero() { + var draft = new StockDraft("article-1", "location-1", null, null, 0); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidMinimumShelfLife.class); + } + + @Test + @DisplayName("should fail when minimumShelfLife is negative") + void shouldFailWhenMinimumShelfLifeNegative() { + var draft = new StockDraft("article-1", "location-1", null, null, -5); + + var result = Stock.create(draft); + + assertThat(result.isFailure()).isTrue(); + assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidMinimumShelfLife.class); + } + + @Test + @DisplayName("should accept minimumShelfLife of 1") + void shouldAcceptMinimumShelfLifeOne() { + var draft = new StockDraft("article-1", "location-1", null, null, 1); + + var result = Stock.create(draft); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.unsafeGetValue().minimumShelfLife().days()).isEqualTo(1); + } + + @Test + @DisplayName("should create with all UnitOfMeasure values") + void shouldCreateWithAllUnits() { + for (UnitOfMeasure unit : UnitOfMeasure.values()) { + var draft = new StockDraft("article-1", "location-1", "5", unit.name(), null); + var result = Stock.create(draft); + assertThat(result.isSuccess()).as("UnitOfMeasure %s should be valid", unit).isTrue(); + } + } + } + + // ==================== Reconstitute ==================== + + @Nested + @DisplayName("reconstitute()") + class Reconstitute { + + @Test + @DisplayName("should reconstitute Stock from persistence") + void shouldReconstitute() { + var id = StockId.generate(); + var articleId = ArticleId.of("article-1"); + var locationId = StorageLocationId.of("location-1"); + var quantity = Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null); + var minimumLevel = new MinimumLevel(quantity); + var minimumShelfLife = new MinimumShelfLife(30); + + var stock = Stock.reconstitute(id, articleId, locationId, minimumLevel, minimumShelfLife); + + assertThat(stock.id()).isEqualTo(id); + assertThat(stock.articleId()).isEqualTo(articleId); + assertThat(stock.storageLocationId()).isEqualTo(locationId); + assertThat(stock.minimumLevel()).isEqualTo(minimumLevel); + assertThat(stock.minimumShelfLife()).isEqualTo(minimumShelfLife); + } + + @Test + @DisplayName("should reconstitute Stock without optional fields") + void shouldReconstituteWithoutOptionals() { + var id = StockId.generate(); + var articleId = ArticleId.of("article-1"); + var locationId = StorageLocationId.of("location-1"); + + var stock = Stock.reconstitute(id, articleId, locationId, null, null); + + assertThat(stock.minimumLevel()).isNull(); + assertThat(stock.minimumShelfLife()).isNull(); + } + } + + // ==================== Equality ==================== + + @Nested + @DisplayName("equals / hashCode") + class Equality { + + @Test + @DisplayName("should be equal if same ID") + void shouldBeEqualBySameId() { + var id = StockId.generate(); + var stock1 = Stock.reconstitute(id, ArticleId.of("a1"), StorageLocationId.of("l1"), null, null); + var stock2 = Stock.reconstitute(id, ArticleId.of("a2"), StorageLocationId.of("l2"), null, null); + + assertThat(stock1).isEqualTo(stock2); + assertThat(stock1.hashCode()).isEqualTo(stock2.hashCode()); + } + + @Test + @DisplayName("should not be equal if different ID") + void shouldNotBeEqualByDifferentId() { + var stock1 = createValidStock(); + var stock2 = createValidStock(); + + assertThat(stock1).isNotEqualTo(stock2); + } + } + + // ==================== Helpers ==================== + + private Stock createValidStock() { + var draft = new StockDraft("article-1", "location-1", "10", "KILOGRAM", 30); + return Stock.create(draft).unsafeGetValue(); + } +} diff --git a/backend/src/test/java/de/effigenix/domain/production/BatchNumberTest.java b/backend/src/test/java/de/effigenix/domain/production/BatchNumberTest.java index c1efa7d..8fba4bc 100644 --- a/backend/src/test/java/de/effigenix/domain/production/BatchNumberTest.java +++ b/backend/src/test/java/de/effigenix/domain/production/BatchNumberTest.java @@ -1,5 +1,6 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.QuantityError; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/de/effigenix/domain/production/IngredientTest.java b/backend/src/test/java/de/effigenix/domain/production/IngredientTest.java index a49e620..9e43eaf 100644 --- a/backend/src/test/java/de/effigenix/domain/production/IngredientTest.java +++ b/backend/src/test/java/de/effigenix/domain/production/IngredientTest.java @@ -1,5 +1,6 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.UnitOfMeasure; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/de/effigenix/domain/production/RecipeTest.java b/backend/src/test/java/de/effigenix/domain/production/RecipeTest.java index cb4fd5d..5c8a42b 100644 --- a/backend/src/test/java/de/effigenix/domain/production/RecipeTest.java +++ b/backend/src/test/java/de/effigenix/domain/production/RecipeTest.java @@ -1,5 +1,7 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.Quantity; +import de.effigenix.shared.common.UnitOfMeasure; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/de/effigenix/domain/production/UnitOfMeasureTest.java b/backend/src/test/java/de/effigenix/domain/production/UnitOfMeasureTest.java index bf02346..a0c93ea 100644 --- a/backend/src/test/java/de/effigenix/domain/production/UnitOfMeasureTest.java +++ b/backend/src/test/java/de/effigenix/domain/production/UnitOfMeasureTest.java @@ -1,5 +1,6 @@ package de.effigenix.domain.production; +import de.effigenix.shared.common.UnitOfMeasure; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/de/effigenix/domain/production/YieldPercentageTest.java b/backend/src/test/java/de/effigenix/domain/production/YieldPercentageTest.java index 9c967ca..d74a442 100644 --- a/backend/src/test/java/de/effigenix/domain/production/YieldPercentageTest.java +++ b/backend/src/test/java/de/effigenix/domain/production/YieldPercentageTest.java @@ -8,7 +8,10 @@ import org.junit.jupiter.params.provider.ValueSource; import java.math.BigDecimal; -import static de.effigenix.domain.production.UnitOfMeasure.*; +import de.effigenix.shared.common.Quantity; +import de.effigenix.shared.common.QuantityError; + +import static de.effigenix.shared.common.UnitOfMeasure.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockControllerIntegrationTest.java b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockControllerIntegrationTest.java new file mode 100644 index 0000000..6b35be0 --- /dev/null +++ b/backend/src/test/java/de/effigenix/infrastructure/inventory/web/StockControllerIntegrationTest.java @@ -0,0 +1,220 @@ +package de.effigenix.infrastructure.inventory.web; + +import de.effigenix.domain.usermanagement.RoleName; +import de.effigenix.infrastructure.AbstractIntegrationTest; +import de.effigenix.infrastructure.inventory.web.dto.CreateStockRequest; +import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity; +import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +import java.util.Set; +import java.util.UUID; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Integrationstests für StockController. + * + * Abgedeckte Testfälle: + * - Story 2.1 – Bestandsposition anlegen + */ +@DisplayName("Stock Controller Integration Tests") +class StockControllerIntegrationTest extends AbstractIntegrationTest { + + private String adminToken; + private String viewerToken; + + private String storageLocationId; + + @BeforeEach + void setUp() throws Exception { + RoleEntity adminRole = createRole(RoleName.ADMIN, "Admin"); + RoleEntity viewerRole = createRole(RoleName.PRODUCTION_WORKER, "Viewer"); + + UserEntity admin = createUser("stock.admin", "stock.admin@test.com", Set.of(adminRole), "BRANCH-01"); + UserEntity viewer = createUser("stock.viewer", "stock.viewer@test.com", Set.of(viewerRole), "BRANCH-01"); + + adminToken = generateToken(admin.getId(), "stock.admin", "STOCK_WRITE,STOCK_READ"); + viewerToken = generateToken(viewer.getId(), "stock.viewer", "USER_READ"); + + storageLocationId = createStorageLocation(); + } + + // ==================== Bestandsposition anlegen – Pflichtfelder ==================== + + @Test + @DisplayName("Bestandsposition mit Pflichtfeldern erstellen → 201") + void createStock_withRequiredFields_returns201() throws Exception { + var request = new CreateStockRequest( + UUID.randomUUID().toString(), storageLocationId, null, null, null); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").isNotEmpty()) + .andExpect(jsonPath("$.articleId").value(request.articleId())) + .andExpect(jsonPath("$.storageLocationId").value(storageLocationId)) + .andExpect(jsonPath("$.minimumLevel").isEmpty()) + .andExpect(jsonPath("$.minimumShelfLifeDays").isEmpty()); + } + + // ==================== Bestandsposition mit allen Feldern ==================== + + @Test + @DisplayName("Bestandsposition mit allen Feldern erstellen → 201") + void createStock_withAllFields_returns201() throws Exception { + var request = new CreateStockRequest( + UUID.randomUUID().toString(), storageLocationId, "10.5", "KILOGRAM", 30); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.articleId").value(request.articleId())) + .andExpect(jsonPath("$.storageLocationId").value(storageLocationId)) + .andExpect(jsonPath("$.minimumLevel.amount").value(10.5)) + .andExpect(jsonPath("$.minimumLevel.unit").value("KILOGRAM")) + .andExpect(jsonPath("$.minimumShelfLifeDays").value(30)); + } + + // ==================== Duplikat ==================== + + @Test + @DisplayName("Bestandsposition Duplikat (gleiche articleId+storageLocationId) → 409") + void createStock_duplicate_returns409() throws Exception { + String articleId = UUID.randomUUID().toString(); + var request = new CreateStockRequest(articleId, storageLocationId, null, null, null); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.code").value("DUPLICATE_STOCK")); + } + + // ==================== Validierungsfehler ==================== + + @Test + @DisplayName("Bestandsposition ohne articleId → 400") + void createStock_withBlankArticleId_returns400() throws Exception { + var request = new CreateStockRequest("", storageLocationId, null, null, null); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Bestandsposition ohne storageLocationId → 400") + void createStock_withBlankStorageLocationId_returns400() throws Exception { + var request = new CreateStockRequest(UUID.randomUUID().toString(), "", null, null, null); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Bestandsposition mit ungültigem MinimumLevel → 400") + void createStock_withInvalidMinimumLevel_returns400() throws Exception { + var request = new CreateStockRequest( + UUID.randomUUID().toString(), storageLocationId, "-1", "KILOGRAM", null); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_MINIMUM_LEVEL")); + } + + @Test + @DisplayName("Bestandsposition mit ungültiger Unit → 400") + void createStock_withInvalidUnit_returns400() throws Exception { + var request = new CreateStockRequest( + UUID.randomUUID().toString(), storageLocationId, "10", "INVALID_UNIT", null); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_MINIMUM_LEVEL")); + } + + @Test + @DisplayName("Bestandsposition mit ungültigem MinimumShelfLife → 400") + void createStock_withInvalidMinimumShelfLife_returns400() throws Exception { + var request = new CreateStockRequest( + UUID.randomUUID().toString(), storageLocationId, null, null, 0); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("INVALID_MINIMUM_SHELF_LIFE")); + } + + // ==================== Autorisierung ==================== + + @Test + @DisplayName("Bestandsposition erstellen ohne STOCK_WRITE → 403") + void createStock_withViewerToken_returns403() throws Exception { + var request = new CreateStockRequest( + UUID.randomUUID().toString(), storageLocationId, null, null, null); + + mockMvc.perform(post("/api/inventory/stocks") + .header("Authorization", "Bearer " + viewerToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("Bestandsposition erstellen ohne Token → 401") + void createStock_withoutToken_returns401() throws Exception { + var request = new CreateStockRequest( + UUID.randomUUID().toString(), storageLocationId, null, null, null); + + mockMvc.perform(post("/api/inventory/stocks") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + } + + // ==================== Hilfsmethoden ==================== + + private String createStorageLocation() throws Exception { + String json = """ + {"name": "Testlager-%s", "storageType": "DRY_STORAGE"} + """.formatted(UUID.randomUUID().toString().substring(0, 8)); + + var result = mockMvc.perform(post("/api/inventory/storage-locations") + .header("Authorization", "Bearer " + adminToken) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andReturn(); + + return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText(); + } +} diff --git a/backend/src/test/java/de/effigenix/domain/production/QuantityTest.java b/backend/src/test/java/de/effigenix/shared/common/QuantityTest.java similarity index 98% rename from backend/src/test/java/de/effigenix/domain/production/QuantityTest.java rename to backend/src/test/java/de/effigenix/shared/common/QuantityTest.java index 5b3329e..38766d8 100644 --- a/backend/src/test/java/de/effigenix/domain/production/QuantityTest.java +++ b/backend/src/test/java/de/effigenix/shared/common/QuantityTest.java @@ -1,13 +1,12 @@ -package de.effigenix.domain.production; +package de.effigenix.shared.common; -import de.effigenix.shared.common.Result; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.math.BigDecimal; -import static de.effigenix.domain.production.UnitOfMeasure.*; +import static de.effigenix.shared.common.UnitOfMeasure.*; import static org.assertj.core.api.Assertions.assertThat; @DisplayName("Quantity Value Object") diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml index 5e3eccb..3d6e3ba 100644 --- a/backend/src/test/resources/application-test.yml +++ b/backend/src/test/resources/application-test.yml @@ -9,7 +9,7 @@ spring: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop - show-sql: true + show-sql: false liquibase: enabled: false # Use Hibernate for test schema diff --git a/frontend/openapi.json b/frontend/openapi.json index 330f7ea..f896926 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":"Product Categories","description":"Product category management endpoints"},{"name":"User Management","description":"User management endpoints (requires authentication)"},{"name":"Articles","description":"Article management endpoints"},{"name":"Recipes","description":"Recipe management endpoints"},{"name":"Role Management","description":"Role management endpoints (ADMIN only)"},{"name":"Customers","description":"Customer management endpoints"},{"name":"Suppliers","description":"Supplier management endpoints"},{"name":"Authentication","description":"Authentication and session 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":{"200":{"description":"User updated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDTO"}}}},"409":{"description":"Email already exists","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}":{"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/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":{"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}/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/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/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/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/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_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},"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":["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"}}},"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":["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"},"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"}}},"CreateStorageLocationRequest":{"required":["name","storageType"],"type":"object","properties":{"name":{"type":"string"},"storageType":{"type":"string"},"minTemperature":{"type":"string"},"maxTemperature":{"type":"string"}}},"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"}}},"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": "Product Categories", "description": "Product category management endpoints"}, {"name": "User Management", "description": "User management endpoints (requires authentication)"}, {"name": "Articles", "description": "Article management endpoints"}, {"name": "Recipes", "description": "Recipe management endpoints"}, {"name": "Role Management", "description": "Role management endpoints (ADMIN only)"}, {"name": "Stocks", "description": "Stock management endpoints"}, {"name": "Customers", "description": "Customer management endpoints"}, {"name": "Suppliers", "description": "Supplier management endpoints"}, {"name": "Authentication", "description": "Authentication and session 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": {"200": {"description": "User updated successfully", "content": {"*/*": {"schema": {"$ref": "#/components/schemas/UserDTO"}}}}, "409": {"description": "Email already exists", "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}": {"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/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": {"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}/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/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/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/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/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": []}]}}, "/api/inventory/stocks": {"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/StockResponse"}}}}}, "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_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}, "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": ["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"}}}, "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": ["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"}, "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"}}}, "CreateStorageLocationRequest": {"required": ["name", "storageType"], "type": "object", "properties": {"name": {"type": "string"}, "storageType": {"type": "string"}, "minTemperature": {"type": "string"}, "maxTemperature": {"type": "string"}}}, "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"}}}, "RemoveCertificateRequest": {"required": ["certificateType"], "type": "object", "properties": {"certificateType": {"type": "string"}, "issuer": {"type": "string"}, "validFrom": {"type": "string", "format": "date"}}}, "CreateStockRequest": {"required": ["articleId", "storageLocationId"], "type": "object", "properties": {"articleId": {"type": "string"}, "storageLocationId": {"type": "string"}, "minimumLevelAmount": {"type": "string"}, "minimumLevelUnit": {"type": "string"}, "minimumShelfLifeDays": {"type": "integer", "format": "int32"}}}, "StockResponse": {"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}}}}, "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/types/src/generated/api.ts b/frontend/packages/types/src/generated/api.ts index eb3f839..4e958f5 100644 --- a/frontend/packages/types/src/generated/api.ts +++ b/frontend/packages/types/src/generated/api.ts @@ -420,6 +420,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/inventory/stocks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createStock"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/customers": { parameters: { query?: never; @@ -1173,6 +1189,26 @@ export interface components { minTemperature?: string; maxTemperature?: string; }; + CreateStockRequest: { + articleId: string; + storageLocationId: string; + minimumLevelAmount?: string; + minimumLevelUnit?: string; + /** Format: int32 */ + minimumShelfLifeDays?: number; + }; + MinimumLevelResponse: { + amount: number; + unit: string; + } | null; + StockResponse: { + id: string; + articleId: string; + storageLocationId: string; + minimumLevel?: components["schemas"]["MinimumLevelResponse"]; + /** Format: int32 */ + minimumShelfLifeDays?: number | null; + }; CreateCustomerRequest: { name: string; /** @enum {string} */ @@ -2265,6 +2301,30 @@ export interface operations { }; }; }; + createStock: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateStockRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["StockResponse"]; + }; + }; + }; + }; listCustomers: { parameters: { query?: {