diff --git a/backend/src/main/java/de/effigenix/infrastructure/config/MasterDataUseCaseConfiguration.java b/backend/src/main/java/de/effigenix/infrastructure/config/MasterDataUseCaseConfiguration.java new file mode 100644 index 0000000..5e67d91 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/config/MasterDataUseCaseConfiguration.java @@ -0,0 +1,93 @@ +package de.effigenix.infrastructure.config; + +import de.effigenix.application.masterdata.*; +import de.effigenix.domain.masterdata.ArticleRepository; +import de.effigenix.domain.masterdata.ProductCategoryRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MasterDataUseCaseConfiguration { + + // ==================== Article Use Cases ==================== + + @Bean + public CreateArticle createArticle(ArticleRepository articleRepository) { + return new CreateArticle(articleRepository); + } + + @Bean + public UpdateArticle updateArticle(ArticleRepository articleRepository) { + return new UpdateArticle(articleRepository); + } + + @Bean + public GetArticle getArticle(ArticleRepository articleRepository) { + return new GetArticle(articleRepository); + } + + @Bean + public ListArticles listArticles(ArticleRepository articleRepository) { + return new ListArticles(articleRepository); + } + + @Bean + public ActivateArticle activateArticle(ArticleRepository articleRepository) { + return new ActivateArticle(articleRepository); + } + + @Bean + public DeactivateArticle deactivateArticle(ArticleRepository articleRepository) { + return new DeactivateArticle(articleRepository); + } + + @Bean + public AddSalesUnit addSalesUnit(ArticleRepository articleRepository) { + return new AddSalesUnit(articleRepository); + } + + @Bean + public RemoveSalesUnit removeSalesUnit(ArticleRepository articleRepository) { + return new RemoveSalesUnit(articleRepository); + } + + @Bean + public UpdateSalesUnitPrice updateSalesUnitPrice(ArticleRepository articleRepository) { + return new UpdateSalesUnitPrice(articleRepository); + } + + @Bean + public AssignSupplier assignSupplier(ArticleRepository articleRepository) { + return new AssignSupplier(articleRepository); + } + + @Bean + public RemoveSupplier removeSupplier(ArticleRepository articleRepository) { + return new RemoveSupplier(articleRepository); + } + + // ==================== ProductCategory Use Cases ==================== + + @Bean + public CreateProductCategory createProductCategory(ProductCategoryRepository categoryRepository) { + return new CreateProductCategory(categoryRepository); + } + + @Bean + public UpdateProductCategory updateProductCategory(ProductCategoryRepository categoryRepository) { + return new UpdateProductCategory(categoryRepository); + } + + @Bean + public ListProductCategories listProductCategories(ProductCategoryRepository categoryRepository) { + return new ListProductCategories(categoryRepository); + } + + @Bean + public DeleteProductCategory deleteProductCategory( + ProductCategoryRepository categoryRepository, + ArticleRepository articleRepository + ) { + return new DeleteProductCategory(categoryRepository, articleRepository); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/ArticleEntity.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/ArticleEntity.java new file mode 100644 index 0000000..ba89c70 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/ArticleEntity.java @@ -0,0 +1,77 @@ +package de.effigenix.infrastructure.masterdata.persistence.entity; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Entity +@Table(name = "articles") +public class ArticleEntity { + + @Id + @Column(name = "id", nullable = false, length = 36) + private String id; + + @Column(name = "name", nullable = false, length = 200) + private String name; + + @Column(name = "article_number", nullable = false, unique = true, length = 50) + private String articleNumber; + + @Column(name = "category_id", nullable = false, length = 36) + private String categoryId; + + @Column(name = "status", nullable = false, length = 20) + private String status; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @OneToMany(mappedBy = "article", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private List salesUnits = new ArrayList<>(); + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "article_suppliers", joinColumns = @JoinColumn(name = "article_id")) + @Column(name = "supplier_id") + private Set supplierIds = new HashSet<>(); + + protected ArticleEntity() {} + + public ArticleEntity(String id, String name, String articleNumber, String categoryId, + String status, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.name = name; + this.articleNumber = articleNumber; + this.categoryId = categoryId; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public String getId() { return id; } + public String getName() { return name; } + public String getArticleNumber() { return articleNumber; } + public String getCategoryId() { return categoryId; } + public String getStatus() { return status; } + public LocalDateTime getCreatedAt() { return createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public List getSalesUnits() { return salesUnits; } + public Set getSupplierIds() { return supplierIds; } + + public void setId(String id) { this.id = id; } + public void setName(String name) { this.name = name; } + public void setArticleNumber(String articleNumber) { this.articleNumber = articleNumber; } + public void setCategoryId(String categoryId) { this.categoryId = categoryId; } + public void setStatus(String status) { this.status = status; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + public void setSalesUnits(List salesUnits) { this.salesUnits = salesUnits; } + public void setSupplierIds(Set supplierIds) { this.supplierIds = supplierIds; } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/ProductCategoryEntity.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/ProductCategoryEntity.java new file mode 100644 index 0000000..287cbbf --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/ProductCategoryEntity.java @@ -0,0 +1,37 @@ +package de.effigenix.infrastructure.masterdata.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "product_categories") +public class ProductCategoryEntity { + + @Id + @Column(name = "id", nullable = false, length = 36) + private String id; + + @Column(name = "name", nullable = false, unique = true, length = 100) + private String name; + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + protected ProductCategoryEntity() {} + + public ProductCategoryEntity(String id, String name, String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public String getId() { return id; } + public String getName() { return name; } + public String getDescription() { return description; } + + public void setId(String id) { this.id = id; } + public void setName(String name) { this.name = name; } + public void setDescription(String description) { this.description = description; } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/SalesUnitEntity.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/SalesUnitEntity.java new file mode 100644 index 0000000..590b802 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/SalesUnitEntity.java @@ -0,0 +1,56 @@ +package de.effigenix.infrastructure.masterdata.persistence.entity; + +import jakarta.persistence.*; + +import java.math.BigDecimal; + +@Entity +@Table(name = "sales_units") +public class SalesUnitEntity { + + @Id + @Column(name = "id", nullable = false, length = 36) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id", nullable = false) + private ArticleEntity article; + + @Column(name = "unit", nullable = false, length = 30) + private String unit; + + @Column(name = "price_model", nullable = false, length = 30) + private String priceModel; + + @Column(name = "price_amount", nullable = false, precision = 19, scale = 2) + private BigDecimal priceAmount; + + @Column(name = "price_currency", nullable = false, length = 3) + private String priceCurrency; + + protected SalesUnitEntity() {} + + public SalesUnitEntity(String id, ArticleEntity article, String unit, String priceModel, + BigDecimal priceAmount, String priceCurrency) { + this.id = id; + this.article = article; + this.unit = unit; + this.priceModel = priceModel; + this.priceAmount = priceAmount; + this.priceCurrency = priceCurrency; + } + + public String getId() { return id; } + public ArticleEntity getArticle() { return article; } + public String getUnit() { return unit; } + public String getPriceModel() { return priceModel; } + public BigDecimal getPriceAmount() { return priceAmount; } + public String getPriceCurrency() { return priceCurrency; } + + public void setId(String id) { this.id = id; } + public void setArticle(ArticleEntity article) { this.article = article; } + public void setUnit(String unit) { this.unit = unit; } + public void setPriceModel(String priceModel) { this.priceModel = priceModel; } + public void setPriceAmount(BigDecimal priceAmount) { this.priceAmount = priceAmount; } + public void setPriceCurrency(String priceCurrency) { this.priceCurrency = priceCurrency; } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/ArticleMapper.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/ArticleMapper.java new file mode 100644 index 0000000..34955d5 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/ArticleMapper.java @@ -0,0 +1,83 @@ +package de.effigenix.infrastructure.masterdata.persistence.mapper; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.infrastructure.masterdata.persistence.entity.ArticleEntity; +import de.effigenix.infrastructure.masterdata.persistence.entity.SalesUnitEntity; +import de.effigenix.shared.common.Money; +import org.springframework.stereotype.Component; + +import java.util.Currency; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class ArticleMapper { + + public ArticleEntity toEntity(Article article) { + var entity = new ArticleEntity( + article.id().value(), + article.name().value(), + article.articleNumber().value(), + article.categoryId().value(), + article.status().name(), + article.createdAt(), + article.updatedAt() + ); + + List salesUnitEntities = article.salesUnits().stream() + .map(su -> toSalesUnitEntity(su, entity)) + .collect(Collectors.toList()); + entity.setSalesUnits(salesUnitEntities); + + Set supplierIds = article.supplierReferences().stream() + .map(SupplierId::value) + .collect(Collectors.toSet()); + entity.setSupplierIds(supplierIds); + + return entity; + } + + public Article toDomain(ArticleEntity entity) { + List salesUnits = entity.getSalesUnits().stream() + .map(this::toDomainSalesUnit) + .collect(Collectors.toList()); + + Set supplierRefs = entity.getSupplierIds().stream() + .map(SupplierId::of) + .collect(Collectors.toSet()); + + return Article.reconstitute( + ArticleId.of(entity.getId()), + new ArticleName(entity.getName()), + new ArticleNumber(entity.getArticleNumber()), + ProductCategoryId.of(entity.getCategoryId()), + salesUnits, + ArticleStatus.valueOf(entity.getStatus()), + supplierRefs, + entity.getCreatedAt(), + entity.getUpdatedAt() + ); + } + + private SalesUnitEntity toSalesUnitEntity(SalesUnit su, ArticleEntity article) { + return new SalesUnitEntity( + su.id().value(), + article, + su.unit().name(), + su.priceModel().name(), + su.price().amount(), + su.price().currency().getCurrencyCode() + ); + } + + private SalesUnit toDomainSalesUnit(SalesUnitEntity entity) { + return SalesUnit.reconstitute( + SalesUnitId.of(entity.getId()), + Unit.valueOf(entity.getUnit()), + PriceModel.valueOf(entity.getPriceModel()), + new Money(entity.getPriceAmount(), Currency.getInstance(entity.getPriceCurrency())) + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/ProductCategoryMapper.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/ProductCategoryMapper.java new file mode 100644 index 0000000..ff45b05 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/ProductCategoryMapper.java @@ -0,0 +1,27 @@ +package de.effigenix.infrastructure.masterdata.persistence.mapper; + +import de.effigenix.domain.masterdata.CategoryName; +import de.effigenix.domain.masterdata.ProductCategory; +import de.effigenix.domain.masterdata.ProductCategoryId; +import de.effigenix.infrastructure.masterdata.persistence.entity.ProductCategoryEntity; +import org.springframework.stereotype.Component; + +@Component +public class ProductCategoryMapper { + + public ProductCategoryEntity toEntity(ProductCategory category) { + return new ProductCategoryEntity( + category.id().value(), + category.name().value(), + category.description() + ); + } + + public ProductCategory toDomain(ProductCategoryEntity entity) { + return ProductCategory.reconstitute( + ProductCategoryId.of(entity.getId()), + new CategoryName(entity.getName()), + entity.getDescription() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/ArticleJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/ArticleJpaRepository.java new file mode 100644 index 0000000..e532df8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/ArticleJpaRepository.java @@ -0,0 +1,15 @@ +package de.effigenix.infrastructure.masterdata.persistence.repository; + +import de.effigenix.infrastructure.masterdata.persistence.entity.ArticleEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ArticleJpaRepository extends JpaRepository { + + List findByCategoryId(String categoryId); + + List findByStatus(String status); + + boolean existsByArticleNumber(String articleNumber); +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaArticleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaArticleRepository.java new file mode 100644 index 0000000..a76f142 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaArticleRepository.java @@ -0,0 +1,103 @@ +package de.effigenix.infrastructure.masterdata.persistence.repository; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.infrastructure.masterdata.persistence.mapper.ArticleMapper; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +@Transactional(readOnly = true) +public class JpaArticleRepository implements ArticleRepository { + + private final ArticleJpaRepository jpaRepository; + private final ArticleMapper mapper; + + public JpaArticleRepository(ArticleJpaRepository jpaRepository, ArticleMapper mapper) { + this.jpaRepository = jpaRepository; + this.mapper = mapper; + } + + @Override + public Result> findById(ArticleId id) { + try { + Optional
result = jpaRepository.findById(id.value()) + .map(mapper::toDomain); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findAll() { + try { + List
result = jpaRepository.findAll().stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findByCategory(ProductCategoryId categoryId) { + try { + List
result = jpaRepository.findByCategoryId(categoryId.value()).stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findByStatus(ArticleStatus status) { + try { + List
result = jpaRepository.findByStatus(status.name()).stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + @Transactional + public Result save(Article article) { + try { + jpaRepository.save(mapper.toEntity(article)); + return Result.success(null); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + @Transactional + public Result delete(Article article) { + try { + jpaRepository.deleteById(article.id().value()); + return Result.success(null); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result existsByArticleNumber(ArticleNumber articleNumber) { + try { + return Result.success(jpaRepository.existsByArticleNumber(articleNumber.value())); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaProductCategoryRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaProductCategoryRepository.java new file mode 100644 index 0000000..a9b71a7 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaProductCategoryRepository.java @@ -0,0 +1,82 @@ +package de.effigenix.infrastructure.masterdata.persistence.repository; + +import de.effigenix.domain.masterdata.CategoryName; +import de.effigenix.domain.masterdata.ProductCategory; +import de.effigenix.domain.masterdata.ProductCategoryId; +import de.effigenix.domain.masterdata.ProductCategoryRepository; +import de.effigenix.infrastructure.masterdata.persistence.mapper.ProductCategoryMapper; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +@Transactional(readOnly = true) +public class JpaProductCategoryRepository implements ProductCategoryRepository { + + private final ProductCategoryJpaRepository jpaRepository; + private final ProductCategoryMapper mapper; + + public JpaProductCategoryRepository(ProductCategoryJpaRepository jpaRepository, ProductCategoryMapper mapper) { + this.jpaRepository = jpaRepository; + this.mapper = mapper; + } + + @Override + public Result> findById(ProductCategoryId id) { + try { + Optional result = jpaRepository.findById(id.value()) + .map(mapper::toDomain); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findAll() { + try { + List result = jpaRepository.findAll().stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + @Transactional + public Result save(ProductCategory category) { + try { + jpaRepository.save(mapper.toEntity(category)); + return Result.success(null); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + @Transactional + public Result delete(ProductCategory category) { + try { + jpaRepository.deleteById(category.id().value()); + return Result.success(null); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result existsByName(CategoryName name) { + try { + return Result.success(jpaRepository.existsByName(name.value())); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/ProductCategoryJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/ProductCategoryJpaRepository.java new file mode 100644 index 0000000..7d78faa --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/ProductCategoryJpaRepository.java @@ -0,0 +1,13 @@ +package de.effigenix.infrastructure.masterdata.persistence.repository; + +import de.effigenix.infrastructure.masterdata.persistence.entity.ProductCategoryEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ProductCategoryJpaRepository extends JpaRepository { + + boolean existsByName(String name); + + Optional findByName(String name); +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java new file mode 100644 index 0000000..5c89128 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ArticleController.java @@ -0,0 +1,323 @@ +package de.effigenix.infrastructure.masterdata.web.controller; + +import de.effigenix.application.masterdata.*; +import de.effigenix.application.masterdata.command.*; +import de.effigenix.domain.masterdata.Article; +import de.effigenix.domain.masterdata.ArticleError; +import de.effigenix.domain.masterdata.ArticleId; +import de.effigenix.domain.masterdata.ArticleStatus; +import de.effigenix.domain.masterdata.ProductCategoryId; +import de.effigenix.domain.masterdata.SalesUnitId; +import de.effigenix.domain.masterdata.SupplierId; +import de.effigenix.infrastructure.masterdata.web.dto.*; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +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.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/articles") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Articles", description = "Article management endpoints") +public class ArticleController { + + private static final Logger logger = LoggerFactory.getLogger(ArticleController.class); + + private final CreateArticle createArticle; + private final UpdateArticle updateArticle; + private final GetArticle getArticle; + private final ListArticles listArticles; + private final ActivateArticle activateArticle; + private final DeactivateArticle deactivateArticle; + private final AddSalesUnit addSalesUnit; + private final RemoveSalesUnit removeSalesUnit; + private final UpdateSalesUnitPrice updateSalesUnitPrice; + private final AssignSupplier assignSupplier; + private final RemoveSupplier removeSupplier; + + public ArticleController( + CreateArticle createArticle, + UpdateArticle updateArticle, + GetArticle getArticle, + ListArticles listArticles, + ActivateArticle activateArticle, + DeactivateArticle deactivateArticle, + AddSalesUnit addSalesUnit, + RemoveSalesUnit removeSalesUnit, + UpdateSalesUnitPrice updateSalesUnitPrice, + AssignSupplier assignSupplier, + RemoveSupplier removeSupplier + ) { + this.createArticle = createArticle; + this.updateArticle = updateArticle; + this.getArticle = getArticle; + this.listArticles = listArticles; + this.activateArticle = activateArticle; + this.deactivateArticle = deactivateArticle; + this.addSalesUnit = addSalesUnit; + this.removeSalesUnit = removeSalesUnit; + this.updateSalesUnitPrice = updateSalesUnitPrice; + this.assignSupplier = assignSupplier; + this.removeSupplier = removeSupplier; + } + + @PostMapping + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity
createArticle( + @Valid @RequestBody CreateArticleRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Creating article: {} by actor: {}", request.articleNumber(), actorId.value()); + + var cmd = new CreateArticleCommand( + request.name(), request.articleNumber(), request.categoryId(), + request.unit(), request.priceModel(), request.price() + ); + var result = createArticle.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Article created: {}", request.articleNumber()); + return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + } + + @GetMapping + public ResponseEntity> listArticles( + @RequestParam(value = "categoryId", required = false) String categoryId, + @RequestParam(value = "status", required = false) ArticleStatus status, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Listing articles by actor: {}", actorId.value()); + + Result> result; + if (categoryId != null) { + result = listArticles.executeByCategory(ProductCategoryId.of(categoryId)); + } else if (status != null) { + result = listArticles.executeByStatus(status); + } else { + result = listArticles.execute(); + } + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @GetMapping("/{id}") + public ResponseEntity
getArticle( + @PathVariable("id") String articleId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Getting article: {} by actor: {}", articleId, actorId.value()); + + var result = getArticle.execute(ArticleId.of(articleId)); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity
updateArticle( + @PathVariable("id") String articleId, + @Valid @RequestBody UpdateArticleRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Updating article: {} by actor: {}", articleId, actorId.value()); + + var cmd = new UpdateArticleCommand(articleId, request.name(), request.categoryId()); + var result = updateArticle.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Article updated: {}", articleId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/activate") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity
activate( + @PathVariable("id") String articleId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Activating article: {} by actor: {}", articleId, actorId.value()); + + var result = activateArticle.execute(ArticleId.of(articleId), actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Article activated: {}", articleId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/deactivate") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity
deactivate( + @PathVariable("id") String articleId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Deactivating article: {} by actor: {}", articleId, actorId.value()); + + var result = deactivateArticle.execute(ArticleId.of(articleId), actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Article deactivated: {}", articleId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/sales-units") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity
addSalesUnit( + @PathVariable("id") String articleId, + @Valid @RequestBody AddSalesUnitRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Adding sales unit to article: {} by actor: {}", articleId, actorId.value()); + + var cmd = new AddSalesUnitCommand(articleId, request.unit(), request.priceModel(), request.price()); + var result = addSalesUnit.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Sales unit added to article: {}", articleId); + return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + } + + @DeleteMapping("/{id}/sales-units/{suId}") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity removeSalesUnit( + @PathVariable("id") String articleId, + @PathVariable("suId") String salesUnitId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Removing sales unit {} from article: {} by actor: {}", salesUnitId, articleId, actorId.value()); + + var cmd = new RemoveSalesUnitCommand(articleId, salesUnitId); + var result = removeSalesUnit.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Sales unit removed from article: {}", articleId); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/{id}/sales-units/{suId}/price") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity
updateSalesUnitPrice( + @PathVariable("id") String articleId, + @PathVariable("suId") String salesUnitId, + @Valid @RequestBody UpdateSalesUnitPriceRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Updating price of sales unit {} in article: {} by actor: {}", salesUnitId, articleId, actorId.value()); + + var cmd = new UpdateSalesUnitPriceCommand(articleId, salesUnitId, request.price()); + var result = updateSalesUnitPrice.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Sales unit price updated in article: {}", articleId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/suppliers") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity
assignSupplier( + @PathVariable("id") String articleId, + @Valid @RequestBody AssignSupplierRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Assigning supplier {} to article: {} by actor: {}", request.supplierId(), articleId, actorId.value()); + + var cmd = new AssignSupplierCommand(articleId, request.supplierId()); + var result = assignSupplier.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Supplier assigned to article: {}", articleId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @DeleteMapping("/{id}/suppliers/{supplierId}") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity removeSupplier( + @PathVariable("id") String articleId, + @PathVariable("supplierId") String supplierId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Removing supplier {} from article: {} by actor: {}", supplierId, articleId, actorId.value()); + + var cmd = new RemoveSupplierCommand(articleId, supplierId); + var result = removeSupplier.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ArticleDomainErrorException(result.unsafeGetError()); + } + + logger.info("Supplier removed from article: {}", articleId); + return ResponseEntity.noContent().build(); + } + + private ActorId extractActorId(Authentication authentication) { + if (authentication == null || authentication.getName() == null) { + throw new IllegalStateException("No authentication found in SecurityContext"); + } + return ActorId.of(authentication.getName()); + } + + public static class ArticleDomainErrorException extends RuntimeException { + private final ArticleError error; + + public ArticleDomainErrorException(ArticleError error) { + super(error.message()); + this.error = error; + } + + public ArticleError getError() { + return error; + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java new file mode 100644 index 0000000..391a3bb --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/ProductCategoryController.java @@ -0,0 +1,146 @@ +package de.effigenix.infrastructure.masterdata.web.controller; + +import de.effigenix.application.masterdata.CreateProductCategory; +import de.effigenix.application.masterdata.DeleteProductCategory; +import de.effigenix.application.masterdata.ListProductCategories; +import de.effigenix.application.masterdata.UpdateProductCategory; +import de.effigenix.application.masterdata.command.CreateProductCategoryCommand; +import de.effigenix.application.masterdata.command.UpdateProductCategoryCommand; +import de.effigenix.domain.masterdata.ProductCategory; +import de.effigenix.domain.masterdata.ProductCategoryId; +import de.effigenix.infrastructure.masterdata.web.dto.CreateProductCategoryRequest; +import de.effigenix.infrastructure.masterdata.web.dto.UpdateProductCategoryRequest; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +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.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/categories") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Product Categories", description = "Product category management endpoints") +public class ProductCategoryController { + + private static final Logger logger = LoggerFactory.getLogger(ProductCategoryController.class); + + private final CreateProductCategory createProductCategory; + private final UpdateProductCategory updateProductCategory; + private final ListProductCategories listProductCategories; + private final DeleteProductCategory deleteProductCategory; + + public ProductCategoryController( + CreateProductCategory createProductCategory, + UpdateProductCategory updateProductCategory, + ListProductCategories listProductCategories, + DeleteProductCategory deleteProductCategory + ) { + this.createProductCategory = createProductCategory; + this.updateProductCategory = updateProductCategory; + this.listProductCategories = listProductCategories; + this.deleteProductCategory = deleteProductCategory; + } + + @PostMapping + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity createCategory( + @Valid @RequestBody CreateProductCategoryRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Creating product category: {} by actor: {}", request.name(), actorId.value()); + + var cmd = new CreateProductCategoryCommand(request.name(), request.description()); + var result = createProductCategory.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ProductCategoryDomainErrorException(result.unsafeGetError()); + } + + logger.info("Product category created: {}", request.name()); + return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + } + + @GetMapping + public ResponseEntity> listCategories(Authentication authentication) { + var actorId = extractActorId(authentication); + logger.info("Listing product categories by actor: {}", actorId.value()); + + var result = listProductCategories.execute(); + + if (result.isFailure()) { + throw new ProductCategoryDomainErrorException(result.unsafeGetError()); + } + + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity updateCategory( + @PathVariable("id") String categoryId, + @Valid @RequestBody UpdateProductCategoryRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Updating product category: {} by actor: {}", categoryId, actorId.value()); + + var cmd = new UpdateProductCategoryCommand(categoryId, request.name(), request.description()); + var result = updateProductCategory.execute(cmd, actorId); + + if (result.isFailure()) { + throw new ProductCategoryDomainErrorException(result.unsafeGetError()); + } + + logger.info("Product category updated: {}", categoryId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity deleteCategory( + @PathVariable("id") String categoryId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Deleting product category: {} by actor: {}", categoryId, actorId.value()); + + var result = deleteProductCategory.execute(ProductCategoryId.of(categoryId), actorId); + + if (result.isFailure()) { + throw new ProductCategoryDomainErrorException(result.unsafeGetError()); + } + + logger.info("Product category deleted: {}", categoryId); + return ResponseEntity.noContent().build(); + } + + private ActorId extractActorId(Authentication authentication) { + if (authentication == null || authentication.getName() == null) { + throw new IllegalStateException("No authentication found in SecurityContext"); + } + return ActorId.of(authentication.getName()); + } + + public static class ProductCategoryDomainErrorException extends RuntimeException { + private final de.effigenix.domain.masterdata.ProductCategoryError error; + + public ProductCategoryDomainErrorException(de.effigenix.domain.masterdata.ProductCategoryError error) { + super(error.message()); + this.error = error; + } + + public de.effigenix.domain.masterdata.ProductCategoryError getError() { + return error; + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddSalesUnitRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddSalesUnitRequest.java new file mode 100644 index 0000000..4c6e610 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddSalesUnitRequest.java @@ -0,0 +1,13 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.PriceModel; +import de.effigenix.domain.masterdata.Unit; +import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; + +public record AddSalesUnitRequest( + @NotNull Unit unit, + @NotNull PriceModel priceModel, + @NotNull BigDecimal price +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AssignSupplierRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AssignSupplierRequest.java new file mode 100644 index 0000000..238265c --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AssignSupplierRequest.java @@ -0,0 +1,7 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import jakarta.validation.constraints.NotBlank; + +public record AssignSupplierRequest( + @NotBlank String supplierId +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateArticleRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateArticleRequest.java new file mode 100644 index 0000000..66d7518 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateArticleRequest.java @@ -0,0 +1,17 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import de.effigenix.domain.masterdata.PriceModel; +import de.effigenix.domain.masterdata.Unit; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; + +public record CreateArticleRequest( + @NotBlank String name, + @NotBlank String articleNumber, + @NotBlank String categoryId, + @NotNull Unit unit, + @NotNull PriceModel priceModel, + @NotNull BigDecimal price +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateProductCategoryRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateProductCategoryRequest.java new file mode 100644 index 0000000..522ef40 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateProductCategoryRequest.java @@ -0,0 +1,8 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import jakarta.validation.constraints.NotBlank; + +public record CreateProductCategoryRequest( + @NotBlank String name, + String description +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateArticleRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateArticleRequest.java new file mode 100644 index 0000000..7ab0386 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateArticleRequest.java @@ -0,0 +1,6 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +public record UpdateArticleRequest( + String name, + String categoryId +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateProductCategoryRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateProductCategoryRequest.java new file mode 100644 index 0000000..0d755b1 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateProductCategoryRequest.java @@ -0,0 +1,6 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +public record UpdateProductCategoryRequest( + String name, + String description +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateSalesUnitPriceRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateSalesUnitPriceRequest.java new file mode 100644 index 0000000..36320e6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateSalesUnitPriceRequest.java @@ -0,0 +1,9 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; + +public record UpdateSalesUnitPriceRequest( + @NotNull BigDecimal price +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/exception/MasterDataErrorHttpStatusMapper.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/exception/MasterDataErrorHttpStatusMapper.java new file mode 100644 index 0000000..7089cee --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/exception/MasterDataErrorHttpStatusMapper.java @@ -0,0 +1,36 @@ +package de.effigenix.infrastructure.masterdata.web.exception; + +import de.effigenix.domain.masterdata.ArticleError; +import de.effigenix.domain.masterdata.ProductCategoryError; + +public final class MasterDataErrorHttpStatusMapper { + + private MasterDataErrorHttpStatusMapper() {} + + public static int toHttpStatus(ArticleError error) { + return switch (error) { + case ArticleError.ArticleNotFound e -> 404; + case ArticleError.SalesUnitNotFound e -> 404; + case ArticleError.ArticleNumberAlreadyExists e -> 409; + case ArticleError.DuplicateSalesUnitType e -> 409; + case ArticleError.MinimumSalesUnitRequired e -> 400; + case ArticleError.InvalidPriceModelCombination e -> 400; + case ArticleError.InvalidPrice e -> 400; + case ArticleError.ValidationFailure e -> 400; + case ArticleError.Unauthorized e -> 403; + case ArticleError.RepositoryFailure e -> 500; + }; + } + + public static int toHttpStatus(ProductCategoryError error) { + return switch (error) { + case ProductCategoryError.CategoryNotFound e -> 404; + case ProductCategoryError.CategoryNameAlreadyExists e -> 409; + case ProductCategoryError.CategoryInUse e -> 409; + case ProductCategoryError.InvalidCategoryName e -> 400; + case ProductCategoryError.ValidationFailure e -> 400; + case ProductCategoryError.Unauthorized e -> 403; + case ProductCategoryError.RepositoryFailure e -> 500; + }; + } +} 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 38e06e1..134269e 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,11 @@ package de.effigenix.infrastructure.usermanagement.web.exception; +import de.effigenix.domain.masterdata.ArticleError; +import de.effigenix.domain.masterdata.ProductCategoryError; import de.effigenix.domain.usermanagement.UserError; +import de.effigenix.infrastructure.masterdata.web.controller.ArticleController; +import de.effigenix.infrastructure.masterdata.web.controller.ProductCategoryController; +import de.effigenix.infrastructure.masterdata.web.exception.MasterDataErrorHttpStatusMapper; import de.effigenix.infrastructure.usermanagement.web.controller.AuthController; import de.effigenix.infrastructure.usermanagement.web.controller.UserController; import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse; @@ -92,9 +97,43 @@ public class GlobalExceptionHandler { return ResponseEntity.status(status).body(errorResponse); } - // Note: UserError and ApplicationError are interfaces, not Throwable - // They are wrapped in RuntimeException subclasses (AuthenticationFailedException, DomainErrorException) - // which are then caught by the handlers above + @ExceptionHandler(ArticleController.ArticleDomainErrorException.class) + public ResponseEntity handleArticleDomainError( + ArticleController.ArticleDomainErrorException ex, + HttpServletRequest request + ) { + ArticleError error = ex.getError(); + int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); + logger.warn("Article domain error: {} - {}", error.code(), error.message()); + + ErrorResponse errorResponse = ErrorResponse.from( + error.code(), + error.message(), + status, + request.getRequestURI() + ); + + return ResponseEntity.status(status).body(errorResponse); + } + + @ExceptionHandler(ProductCategoryController.ProductCategoryDomainErrorException.class) + public ResponseEntity handleProductCategoryDomainError( + ProductCategoryController.ProductCategoryDomainErrorException ex, + HttpServletRequest request + ) { + ProductCategoryError error = ex.getError(); + int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); + logger.warn("ProductCategory domain error: {} - {}", error.code(), error.message()); + + ErrorResponse errorResponse = ErrorResponse.from( + error.code(), + error.message(), + status, + request.getRequestURI() + ); + + return ResponseEntity.status(status).body(errorResponse); + } /** * Handles validation errors from @Valid annotations. diff --git a/backend/src/main/resources/db/changelog/changes/005-create-masterdata-schema.xml b/backend/src/main/resources/db/changelog/changes/005-create-masterdata-schema.xml new file mode 100644 index 0000000..0f4d21b --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/005-create-masterdata-schema.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ALTER TABLE articles ADD CONSTRAINT chk_article_status CHECK (status IN ('ACTIVE', 'INACTIVE')); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 df04b4f..0f4a908 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.xml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.xml @@ -9,5 +9,6 @@ +