From b813fcbcaafc45b5a997fe2e85922a8ce1b786f2 Mon Sep 17 00:00:00 2001 From: Sebastian Frick Date: Tue, 17 Feb 2026 22:36:51 +0100 Subject: [PATCH] feat: implement Master Data BC domain model and application layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Master Data BC als Supporting Domain für Artikel, Lieferanten und Kunden. Shared Kernel um Money, Address, ContactInfo, PaymentTerms erweitert. RepositoryError von domain.usermanagement nach shared.common migriert. --- .../masterdata/ActivateArticle.java | 43 ++++ .../masterdata/ActivateCustomer.java | 43 ++++ .../masterdata/ActivateSupplier.java | 43 ++++ .../masterdata/AddCertificate.java | 49 ++++ .../masterdata/AddDeliveryAddress.java | 50 ++++ .../application/masterdata/AddSalesUnit.java | 56 +++++ .../masterdata/AssignSupplier.java | 46 ++++ .../application/masterdata/CreateArticle.java | 62 +++++ .../masterdata/CreateCustomer.java | 52 ++++ .../masterdata/CreateProductCategory.java | 49 ++++ .../masterdata/CreateSupplier.java | 52 ++++ .../masterdata/DeactivateArticle.java | 43 ++++ .../masterdata/DeactivateCustomer.java | 43 ++++ .../masterdata/DeactivateSupplier.java | 43 ++++ .../masterdata/DeleteProductCategory.java | 58 +++++ .../application/masterdata/GetArticle.java | 31 +++ .../application/masterdata/GetCustomer.java | 31 +++ .../application/masterdata/GetSupplier.java | 31 +++ .../application/masterdata/ListArticles.java | 47 ++++ .../application/masterdata/ListCustomers.java | 47 ++++ .../masterdata/ListProductCategories.java | 29 +++ .../application/masterdata/ListSuppliers.java | 38 +++ .../application/masterdata/RateSupplier.java | 52 ++++ .../masterdata/RemoveCertificate.java | 53 +++++ .../masterdata/RemoveDeliveryAddress.java | 46 ++++ .../masterdata/RemoveFrameContract.java | 43 ++++ .../masterdata/RemoveSalesUnit.java | 50 ++++ .../masterdata/RemoveSupplier.java | 46 ++++ .../masterdata/SetFrameContract.java | 65 +++++ .../masterdata/SetPreferences.java | 46 ++++ .../application/masterdata/UpdateArticle.java | 52 ++++ .../masterdata/UpdateCustomer.java | 51 ++++ .../masterdata/UpdateProductCategory.java | 52 ++++ .../masterdata/UpdateSalesUnitPrice.java | 52 ++++ .../masterdata/UpdateSupplier.java | 51 ++++ .../command/AddCertificateCommand.java | 11 + .../command/AddDeliveryAddressCommand.java | 13 + .../command/AddSalesUnitCommand.java | 13 + .../command/AssignSupplierCommand.java | 6 + .../command/CreateArticleCommand.java | 15 ++ .../command/CreateCustomerCommand.java | 18 ++ .../command/CreateProductCategoryCommand.java | 6 + .../command/CreateSupplierCommand.java | 15 ++ .../command/RateSupplierCommand.java | 8 + .../command/RemoveCertificateCommand.java | 10 + .../command/RemoveDeliveryAddressCommand.java | 6 + .../command/RemoveSalesUnitCommand.java | 6 + .../command/RemoveSupplierCommand.java | 6 + .../command/SetFrameContractCommand.java | 23 ++ .../command/SetPreferencesCommand.java | 10 + .../command/UpdateArticleCommand.java | 7 + .../command/UpdateCustomerCommand.java | 16 ++ .../command/UpdateProductCategoryCommand.java | 7 + .../command/UpdateSalesUnitPriceCommand.java | 9 + .../command/UpdateSupplierCommand.java | 16 ++ .../usermanagement/AssignRole.java | 1 + .../usermanagement/AuthenticateUser.java | 1 + .../usermanagement/ChangePassword.java | 1 + .../usermanagement/CreateUser.java | 1 + .../application/usermanagement/GetUser.java | 1 + .../application/usermanagement/ListUsers.java | 2 +- .../application/usermanagement/LockUser.java | 1 + .../usermanagement/RemoveRole.java | 1 + .../usermanagement/UnlockUser.java | 1 + .../usermanagement/UpdateUser.java | 1 + .../effigenix/domain/masterdata/Article.java | 204 ++++++++++++++++ .../domain/masterdata/ArticleError.java | 50 ++++ .../domain/masterdata/ArticleId.java | 20 ++ .../domain/masterdata/ArticleName.java | 13 + .../domain/masterdata/ArticleNumber.java | 13 + .../domain/masterdata/ArticleRepository.java | 24 ++ .../domain/masterdata/ArticleStatus.java | 6 + .../domain/masterdata/CategoryName.java | 13 + .../domain/masterdata/ContractLineItem.java | 28 +++ .../effigenix/domain/masterdata/Customer.java | 225 ++++++++++++++++++ .../domain/masterdata/CustomerError.java | 40 ++++ .../domain/masterdata/CustomerId.java | 20 ++ .../domain/masterdata/CustomerName.java | 13 + .../domain/masterdata/CustomerPreference.java | 11 + .../domain/masterdata/CustomerRepository.java | 24 ++ .../domain/masterdata/CustomerStatus.java | 6 + .../domain/masterdata/CustomerType.java | 6 + .../domain/masterdata/DeliveryAddress.java | 20 ++ .../domain/masterdata/DeliveryRhythm.java | 9 + .../domain/masterdata/FrameContract.java | 111 +++++++++ .../domain/masterdata/FrameContractId.java | 20 ++ .../domain/masterdata/MasterDataAction.java | 24 ++ .../domain/masterdata/PriceModel.java | 6 + .../domain/masterdata/ProductCategory.java | 60 +++++ .../masterdata/ProductCategoryError.java | 35 +++ .../domain/masterdata/ProductCategoryId.java | 20 ++ .../masterdata/ProductCategoryRepository.java | 20 ++ .../domain/masterdata/QualityCertificate.java | 33 +++ .../domain/masterdata/SalesUnit.java | 77 ++++++ .../domain/masterdata/SalesUnitId.java | 20 ++ .../effigenix/domain/masterdata/Supplier.java | 186 +++++++++++++++ .../domain/masterdata/SupplierError.java | 30 +++ .../domain/masterdata/SupplierId.java | 20 ++ .../domain/masterdata/SupplierName.java | 13 + .../domain/masterdata/SupplierRating.java | 23 ++ .../domain/masterdata/SupplierRepository.java | 22 ++ .../domain/masterdata/SupplierStatus.java | 6 + .../de/effigenix/domain/masterdata/Unit.java | 8 + .../domain/usermanagement/RoleRepository.java | 1 + .../domain/usermanagement/UserRepository.java | 1 + .../repository/JpaRoleRepository.java | 2 +- .../repository/JpaUserRepository.java | 2 +- .../web/controller/RoleController.java | 2 +- .../de/effigenix/shared/common/Address.java | 33 +++ .../effigenix/shared/common/ContactInfo.java | 23 ++ .../de/effigenix/shared/common/Money.java | 46 ++++ .../effigenix/shared/common/PaymentTerms.java | 14 ++ .../common}/RepositoryError.java | 3 +- 113 files changed, 3478 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/ActivateArticle.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/ActivateCustomer.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/ActivateSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/AddCertificate.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/AddDeliveryAddress.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/AddSalesUnit.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/AssignSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/CreateArticle.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/CreateCustomer.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/CreateProductCategory.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/CreateSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/DeactivateArticle.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/DeactivateCustomer.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/DeactivateSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/DeleteProductCategory.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/GetArticle.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/GetCustomer.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/GetSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/ListArticles.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/ListCustomers.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/ListProductCategories.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/ListSuppliers.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/RateSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/RemoveCertificate.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/RemoveDeliveryAddress.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/RemoveFrameContract.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/RemoveSalesUnit.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/RemoveSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/SetFrameContract.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/SetPreferences.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/UpdateArticle.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/UpdateCustomer.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/UpdateProductCategory.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/UpdateSalesUnitPrice.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/UpdateSupplier.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/AddCertificateCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/AddDeliveryAddressCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/AddSalesUnitCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/AssignSupplierCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/CreateArticleCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/CreateCustomerCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/CreateProductCategoryCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/CreateSupplierCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/RateSupplierCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/RemoveCertificateCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/RemoveDeliveryAddressCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSalesUnitCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSupplierCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/SetFrameContractCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/SetPreferencesCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/UpdateArticleCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/UpdateCustomerCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/UpdateProductCategoryCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSalesUnitPriceCommand.java create mode 100644 backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSupplierCommand.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/Article.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ArticleError.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ArticleId.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ArticleName.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ArticleNumber.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ArticleRepository.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ArticleStatus.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CategoryName.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ContractLineItem.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/Customer.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CustomerError.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CustomerId.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CustomerName.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CustomerPreference.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CustomerRepository.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CustomerStatus.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/CustomerType.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/DeliveryAddress.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/DeliveryRhythm.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/FrameContract.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/FrameContractId.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/MasterDataAction.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/PriceModel.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ProductCategory.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryError.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryId.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryRepository.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/QualityCertificate.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SalesUnit.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SalesUnitId.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/Supplier.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SupplierError.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SupplierId.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SupplierName.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SupplierRating.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SupplierRepository.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/SupplierStatus.java create mode 100644 backend/src/main/java/de/effigenix/domain/masterdata/Unit.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/Address.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/ContactInfo.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/Money.java create mode 100644 backend/src/main/java/de/effigenix/shared/common/PaymentTerms.java rename backend/src/main/java/de/effigenix/{domain/usermanagement => shared/common}/RepositoryError.java (83%) diff --git a/backend/src/main/java/de/effigenix/application/masterdata/ActivateArticle.java b/backend/src/main/java/de/effigenix/application/masterdata/ActivateArticle.java new file mode 100644 index 0000000..a2becc7 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/ActivateArticle.java @@ -0,0 +1,43 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class ActivateArticle { + + private final ArticleRepository articleRepository; + + public ActivateArticle(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(ArticleId articleId, ActorId performedBy) { + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + article.activate(); + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/ActivateCustomer.java b/backend/src/main/java/de/effigenix/application/masterdata/ActivateCustomer.java new file mode 100644 index 0000000..01fb9ed --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/ActivateCustomer.java @@ -0,0 +1,43 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class ActivateCustomer { + + private final CustomerRepository customerRepository; + + public ActivateCustomer(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(CustomerId customerId, ActorId performedBy) { + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + customer.activate(); + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/ActivateSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/ActivateSupplier.java new file mode 100644 index 0000000..94b2fdb --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/ActivateSupplier.java @@ -0,0 +1,43 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class ActivateSupplier { + + private final SupplierRepository supplierRepository; + + public ActivateSupplier(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(SupplierId supplierId, ActorId performedBy) { + switch (supplierRepository.findById(supplierId)) { + case Failure> f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new SupplierError.SupplierNotFound(supplierId)); + } + Supplier supplier = s.value().get(); + supplier.activate(); + + switch (supplierRepository.save(supplier)) { + case Failure f2 -> + { return Result.failure(new SupplierError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(supplier); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/AddCertificate.java b/backend/src/main/java/de/effigenix/application/masterdata/AddCertificate.java new file mode 100644 index 0000000..0c260f9 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/AddCertificate.java @@ -0,0 +1,49 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.AddCertificateCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class AddCertificate { + + private final SupplierRepository supplierRepository; + + public AddCertificate(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(AddCertificateCommand cmd, ActorId performedBy) { + var supplierId = SupplierId.of(cmd.supplierId()); + + switch (supplierRepository.findById(supplierId)) { + case Failure> f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new SupplierError.SupplierNotFound(supplierId)); + } + Supplier supplier = s.value().get(); + + var certificate = new QualityCertificate( + cmd.certificateType(), cmd.issuer(), cmd.validFrom(), cmd.validUntil()); + supplier.addCertificate(certificate); + + switch (supplierRepository.save(supplier)) { + case Failure f2 -> + { return Result.failure(new SupplierError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(supplier); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/AddDeliveryAddress.java b/backend/src/main/java/de/effigenix/application/masterdata/AddDeliveryAddress.java new file mode 100644 index 0000000..609f4d6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/AddDeliveryAddress.java @@ -0,0 +1,50 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.AddDeliveryAddressCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.Address; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class AddDeliveryAddress { + + private final CustomerRepository customerRepository; + + public AddDeliveryAddress(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(AddDeliveryAddressCommand cmd, ActorId performedBy) { + var customerId = CustomerId.of(cmd.customerId()); + + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + + var address = new Address(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country()); + var deliveryAddress = new DeliveryAddress(cmd.label(), address, cmd.contactPerson(), cmd.deliveryNotes()); + customer.addDeliveryAddress(deliveryAddress); + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/AddSalesUnit.java b/backend/src/main/java/de/effigenix/application/masterdata/AddSalesUnit.java new file mode 100644 index 0000000..430f405 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/AddSalesUnit.java @@ -0,0 +1,56 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.AddSalesUnitCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class AddSalesUnit { + + private final ArticleRepository articleRepository; + + public AddSalesUnit(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(AddSalesUnitCommand cmd, ActorId performedBy) { + var articleId = ArticleId.of(cmd.articleId()); + + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + + var suResult = SalesUnit.create(cmd.unit(), cmd.priceModel(), Money.euro(cmd.price())); + if (suResult.isFailure()) { + return Result.failure(suResult.unsafeGetError()); + } + + var addResult = article.addSalesUnit(suResult.unsafeGetValue()); + if (addResult.isFailure()) { + return Result.failure(addResult.unsafeGetError()); + } + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/AssignSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/AssignSupplier.java new file mode 100644 index 0000000..9d45500 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/AssignSupplier.java @@ -0,0 +1,46 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.AssignSupplierCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class AssignSupplier { + + private final ArticleRepository articleRepository; + + public AssignSupplier(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(AssignSupplierCommand cmd, ActorId performedBy) { + var articleId = ArticleId.of(cmd.articleId()); + + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + article.addSupplierReference(SupplierId.of(cmd.supplierId())); + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/CreateArticle.java b/backend/src/main/java/de/effigenix/application/masterdata/CreateArticle.java new file mode 100644 index 0000000..e77e127 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/CreateArticle.java @@ -0,0 +1,62 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.CreateArticleCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class CreateArticle { + + private final ArticleRepository articleRepository; + + public CreateArticle(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(CreateArticleCommand cmd, ActorId performedBy) { + // Check uniqueness + switch (articleRepository.existsByArticleNumber(new ArticleNumber(cmd.articleNumber()))) { + case Failure f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success s -> { + if (s.value()) { + return Result.failure(new ArticleError.ArticleNumberAlreadyExists(cmd.articleNumber())); + } + } + } + + // Create initial SalesUnit + var salesUnitResult = SalesUnit.create(cmd.unit(), cmd.priceModel(), Money.euro(cmd.price())); + if (salesUnitResult.isFailure()) { + return Result.failure(salesUnitResult.unsafeGetError()); + } + + // Create Article + var articleResult = Article.create( + new ArticleName(cmd.name()), + new ArticleNumber(cmd.articleNumber()), + ProductCategoryId.of(cmd.categoryId()), + salesUnitResult.unsafeGetValue() + ); + if (articleResult.isFailure()) { + return Result.failure(articleResult.unsafeGetError()); + } + + Article article = articleResult.unsafeGetValue(); + + // Save + switch (articleRepository.save(article)) { + case Failure f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/CreateCustomer.java b/backend/src/main/java/de/effigenix/application/masterdata/CreateCustomer.java new file mode 100644 index 0000000..7b69083 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/CreateCustomer.java @@ -0,0 +1,52 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.CreateCustomerCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.*; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class CreateCustomer { + + private final CustomerRepository customerRepository; + + public CreateCustomer(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(CreateCustomerCommand cmd, ActorId performedBy) { + var name = new CustomerName(cmd.name()); + + switch (customerRepository.existsByName(name)) { + case Failure f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success s -> { + if (s.value()) { + return Result.failure(new CustomerError.CustomerNameAlreadyExists(cmd.name())); + } + } + } + + var address = new Address(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country()); + var contactInfo = new ContactInfo(cmd.phone(), cmd.email(), cmd.contactPerson()); + var paymentTerms = new PaymentTerms(cmd.paymentDueDays(), cmd.paymentDescription()); + + var result = Customer.create(name, cmd.type(), address, contactInfo, paymentTerms); + if (result.isFailure()) { + return result; + } + + Customer customer = result.unsafeGetValue(); + + switch (customerRepository.save(customer)) { + case Failure f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/CreateProductCategory.java b/backend/src/main/java/de/effigenix/application/masterdata/CreateProductCategory.java new file mode 100644 index 0000000..705eb4d --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/CreateProductCategory.java @@ -0,0 +1,49 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.CreateProductCategoryCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class CreateProductCategory { + + private final ProductCategoryRepository categoryRepository; + + public CreateProductCategory(ProductCategoryRepository categoryRepository) { + this.categoryRepository = categoryRepository; + } + + public Result execute(CreateProductCategoryCommand cmd, ActorId performedBy) { + var name = new CategoryName(cmd.name()); + + switch (categoryRepository.existsByName(name)) { + case Failure f -> + { return Result.failure(new ProductCategoryError.RepositoryFailure(f.error().message())); } + case Success s -> { + if (s.value()) { + return Result.failure(new ProductCategoryError.CategoryNameAlreadyExists(cmd.name())); + } + } + } + + var result = ProductCategory.create(name, cmd.description()); + if (result.isFailure()) { + return result; + } + + ProductCategory category = result.unsafeGetValue(); + + switch (categoryRepository.save(category)) { + case Failure f -> + { return Result.failure(new ProductCategoryError.RepositoryFailure(f.error().message())); } + case Success ignored -> { } + } + + return Result.success(category); + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/CreateSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/CreateSupplier.java new file mode 100644 index 0000000..197233d --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/CreateSupplier.java @@ -0,0 +1,52 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.CreateSupplierCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.*; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class CreateSupplier { + + private final SupplierRepository supplierRepository; + + public CreateSupplier(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(CreateSupplierCommand cmd, ActorId performedBy) { + var name = new SupplierName(cmd.name()); + + switch (supplierRepository.existsByName(name)) { + case Failure f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success s -> { + if (s.value()) { + return Result.failure(new SupplierError.SupplierNameAlreadyExists(cmd.name())); + } + } + } + + var address = new Address(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country()); + var contactInfo = new ContactInfo(cmd.phone(), cmd.email(), cmd.contactPerson()); + var paymentTerms = new PaymentTerms(cmd.paymentDueDays(), cmd.paymentDescription()); + + var result = Supplier.create(name, address, contactInfo, paymentTerms); + if (result.isFailure()) { + return result; + } + + Supplier supplier = result.unsafeGetValue(); + + switch (supplierRepository.save(supplier)) { + case Failure f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success ignored -> { } + } + + return Result.success(supplier); + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/DeactivateArticle.java b/backend/src/main/java/de/effigenix/application/masterdata/DeactivateArticle.java new file mode 100644 index 0000000..e47856f --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/DeactivateArticle.java @@ -0,0 +1,43 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class DeactivateArticle { + + private final ArticleRepository articleRepository; + + public DeactivateArticle(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(ArticleId articleId, ActorId performedBy) { + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + article.deactivate(); + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/DeactivateCustomer.java b/backend/src/main/java/de/effigenix/application/masterdata/DeactivateCustomer.java new file mode 100644 index 0000000..0abbaa7 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/DeactivateCustomer.java @@ -0,0 +1,43 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class DeactivateCustomer { + + private final CustomerRepository customerRepository; + + public DeactivateCustomer(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(CustomerId customerId, ActorId performedBy) { + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + customer.deactivate(); + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/DeactivateSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/DeactivateSupplier.java new file mode 100644 index 0000000..f2b3f41 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/DeactivateSupplier.java @@ -0,0 +1,43 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class DeactivateSupplier { + + private final SupplierRepository supplierRepository; + + public DeactivateSupplier(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(SupplierId supplierId, ActorId performedBy) { + switch (supplierRepository.findById(supplierId)) { + case Failure> f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new SupplierError.SupplierNotFound(supplierId)); + } + Supplier supplier = s.value().get(); + supplier.deactivate(); + + switch (supplierRepository.save(supplier)) { + case Failure f2 -> + { return Result.failure(new SupplierError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(supplier); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/DeleteProductCategory.java b/backend/src/main/java/de/effigenix/application/masterdata/DeleteProductCategory.java new file mode 100644 index 0000000..244f428 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/DeleteProductCategory.java @@ -0,0 +1,58 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class DeleteProductCategory { + + private final ProductCategoryRepository categoryRepository; + private final ArticleRepository articleRepository; + + public DeleteProductCategory(ProductCategoryRepository categoryRepository, ArticleRepository articleRepository) { + this.categoryRepository = categoryRepository; + this.articleRepository = articleRepository; + } + + public Result execute(ProductCategoryId categoryId, ActorId performedBy) { + // Check category exists + switch (categoryRepository.findById(categoryId)) { + case Failure> f -> + { return Result.failure(new ProductCategoryError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ProductCategoryError.CategoryNotFound(categoryId)); + } + ProductCategory category = s.value().get(); + + // Check no articles are using this category + switch (articleRepository.findByCategory(categoryId)) { + case Failure> f2 -> + { return Result.failure(new ProductCategoryError.RepositoryFailure(f2.error().message())); } + case Success> s2 -> { + if (!s2.value().isEmpty()) { + return Result.failure(new ProductCategoryError.CategoryInUse(categoryId)); + } + } + } + + // Delete + switch (categoryRepository.delete(category)) { + case Failure f3 -> + { return Result.failure(new ProductCategoryError.RepositoryFailure(f3.error().message())); } + case Success ignored -> { } + } + + return Result.success(null); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/GetArticle.java b/backend/src/main/java/de/effigenix/application/masterdata/GetArticle.java new file mode 100644 index 0000000..68cecc3 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/GetArticle.java @@ -0,0 +1,31 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional(readOnly = true) +public class GetArticle { + + private final ArticleRepository articleRepository; + + public GetArticle(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(ArticleId articleId) { + return switch (articleRepository.findById(articleId)) { + case Failure> f -> + Result.failure(new ArticleError.RepositoryFailure(f.error().message())); + case Success> s -> + s.value() + .map(Result::success) + .orElseGet(() -> Result.failure(new ArticleError.ArticleNotFound(articleId))); + }; + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/GetCustomer.java b/backend/src/main/java/de/effigenix/application/masterdata/GetCustomer.java new file mode 100644 index 0000000..2fc3170 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/GetCustomer.java @@ -0,0 +1,31 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional(readOnly = true) +public class GetCustomer { + + private final CustomerRepository customerRepository; + + public GetCustomer(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(CustomerId customerId) { + return switch (customerRepository.findById(customerId)) { + case Failure> f -> + Result.failure(new CustomerError.RepositoryFailure(f.error().message())); + case Success> s -> + s.value() + .map(Result::success) + .orElseGet(() -> Result.failure(new CustomerError.CustomerNotFound(customerId))); + }; + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/GetSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/GetSupplier.java new file mode 100644 index 0000000..315c4f9 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/GetSupplier.java @@ -0,0 +1,31 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional(readOnly = true) +public class GetSupplier { + + private final SupplierRepository supplierRepository; + + public GetSupplier(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(SupplierId supplierId) { + return switch (supplierRepository.findById(supplierId)) { + case Failure> f -> + Result.failure(new SupplierError.RepositoryFailure(f.error().message())); + case Success> s -> + s.value() + .map(Result::success) + .orElseGet(() -> Result.failure(new SupplierError.SupplierNotFound(supplierId))); + }; + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/ListArticles.java b/backend/src/main/java/de/effigenix/application/masterdata/ListArticles.java new file mode 100644 index 0000000..4608c00 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/ListArticles.java @@ -0,0 +1,47 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static de.effigenix.shared.common.Result.*; + +@Transactional(readOnly = true) +public class ListArticles { + + private final ArticleRepository articleRepository; + + public ListArticles(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result> execute() { + return switch (articleRepository.findAll()) { + case Failure> f -> + Result.failure(new ArticleError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } + + public Result> executeByCategory(ProductCategoryId categoryId) { + return switch (articleRepository.findByCategory(categoryId)) { + case Failure> f -> + Result.failure(new ArticleError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } + + public Result> executeByStatus(ArticleStatus status) { + return switch (articleRepository.findByStatus(status)) { + case Failure> f -> + Result.failure(new ArticleError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/ListCustomers.java b/backend/src/main/java/de/effigenix/application/masterdata/ListCustomers.java new file mode 100644 index 0000000..4018796 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/ListCustomers.java @@ -0,0 +1,47 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static de.effigenix.shared.common.Result.*; + +@Transactional(readOnly = true) +public class ListCustomers { + + private final CustomerRepository customerRepository; + + public ListCustomers(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result> execute() { + return switch (customerRepository.findAll()) { + case Failure> f -> + Result.failure(new CustomerError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } + + public Result> executeByType(CustomerType type) { + return switch (customerRepository.findByType(type)) { + case Failure> f -> + Result.failure(new CustomerError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } + + public Result> executeByStatus(CustomerStatus status) { + return switch (customerRepository.findByStatus(status)) { + case Failure> f -> + Result.failure(new CustomerError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/ListProductCategories.java b/backend/src/main/java/de/effigenix/application/masterdata/ListProductCategories.java new file mode 100644 index 0000000..9c115f4 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/ListProductCategories.java @@ -0,0 +1,29 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static de.effigenix.shared.common.Result.*; + +@Transactional(readOnly = true) +public class ListProductCategories { + + private final ProductCategoryRepository categoryRepository; + + public ListProductCategories(ProductCategoryRepository categoryRepository) { + this.categoryRepository = categoryRepository; + } + + public Result> execute() { + return switch (categoryRepository.findAll()) { + case Failure> f -> + Result.failure(new ProductCategoryError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/ListSuppliers.java b/backend/src/main/java/de/effigenix/application/masterdata/ListSuppliers.java new file mode 100644 index 0000000..761f2f8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/ListSuppliers.java @@ -0,0 +1,38 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static de.effigenix.shared.common.Result.*; + +@Transactional(readOnly = true) +public class ListSuppliers { + + private final SupplierRepository supplierRepository; + + public ListSuppliers(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result> execute() { + return switch (supplierRepository.findAll()) { + case Failure> f -> + Result.failure(new SupplierError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } + + public Result> executeByStatus(SupplierStatus status) { + return switch (supplierRepository.findByStatus(status)) { + case Failure> f -> + Result.failure(new SupplierError.RepositoryFailure(f.error().message())); + case Success> s -> + Result.success(s.value()); + }; + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/RateSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/RateSupplier.java new file mode 100644 index 0000000..952a19a --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/RateSupplier.java @@ -0,0 +1,52 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.RateSupplierCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class RateSupplier { + + private final SupplierRepository supplierRepository; + + public RateSupplier(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(RateSupplierCommand cmd, ActorId performedBy) { + var supplierId = SupplierId.of(cmd.supplierId()); + + switch (supplierRepository.findById(supplierId)) { + case Failure> f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new SupplierError.SupplierNotFound(supplierId)); + } + Supplier supplier = s.value().get(); + + try { + var rating = new SupplierRating(cmd.qualityScore(), cmd.deliveryScore(), cmd.priceScore()); + supplier.rate(rating); + } catch (IllegalArgumentException e) { + return Result.failure(new SupplierError.InvalidRating(e.getMessage())); + } + + switch (supplierRepository.save(supplier)) { + case Failure f2 -> + { return Result.failure(new SupplierError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(supplier); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/RemoveCertificate.java b/backend/src/main/java/de/effigenix/application/masterdata/RemoveCertificate.java new file mode 100644 index 0000000..9221a49 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/RemoveCertificate.java @@ -0,0 +1,53 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.RemoveCertificateCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class RemoveCertificate { + + private final SupplierRepository supplierRepository; + + public RemoveCertificate(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(RemoveCertificateCommand cmd, ActorId performedBy) { + var supplierId = SupplierId.of(cmd.supplierId()); + + switch (supplierRepository.findById(supplierId)) { + case Failure> f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new SupplierError.SupplierNotFound(supplierId)); + } + Supplier supplier = s.value().get(); + + // Find matching certificate by type, issuer, and validFrom + supplier.certificates().stream() + .filter(c -> c.certificateType().equals(cmd.certificateType()) + && java.util.Objects.equals(c.issuer(), cmd.issuer()) + && java.util.Objects.equals(c.validFrom(), cmd.validFrom())) + .findFirst() + .ifPresent(supplier::removeCertificate); + + switch (supplierRepository.save(supplier)) { + case Failure f2 -> + { return Result.failure(new SupplierError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(supplier); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/RemoveDeliveryAddress.java b/backend/src/main/java/de/effigenix/application/masterdata/RemoveDeliveryAddress.java new file mode 100644 index 0000000..d85f8d8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/RemoveDeliveryAddress.java @@ -0,0 +1,46 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.RemoveDeliveryAddressCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class RemoveDeliveryAddress { + + private final CustomerRepository customerRepository; + + public RemoveDeliveryAddress(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(RemoveDeliveryAddressCommand cmd, ActorId performedBy) { + var customerId = CustomerId.of(cmd.customerId()); + + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + customer.removeDeliveryAddress(cmd.label()); + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/RemoveFrameContract.java b/backend/src/main/java/de/effigenix/application/masterdata/RemoveFrameContract.java new file mode 100644 index 0000000..d28cc75 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/RemoveFrameContract.java @@ -0,0 +1,43 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class RemoveFrameContract { + + private final CustomerRepository customerRepository; + + public RemoveFrameContract(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(CustomerId customerId, ActorId performedBy) { + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + customer.removeFrameContract(); + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/RemoveSalesUnit.java b/backend/src/main/java/de/effigenix/application/masterdata/RemoveSalesUnit.java new file mode 100644 index 0000000..a1657fa --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/RemoveSalesUnit.java @@ -0,0 +1,50 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.RemoveSalesUnitCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class RemoveSalesUnit { + + private final ArticleRepository articleRepository; + + public RemoveSalesUnit(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(RemoveSalesUnitCommand cmd, ActorId performedBy) { + var articleId = ArticleId.of(cmd.articleId()); + + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + + var removeResult = article.removeSalesUnit(SalesUnitId.of(cmd.salesUnitId())); + if (removeResult.isFailure()) { + return Result.failure(removeResult.unsafeGetError()); + } + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/RemoveSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/RemoveSupplier.java new file mode 100644 index 0000000..0ee6fd5 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/RemoveSupplier.java @@ -0,0 +1,46 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.RemoveSupplierCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class RemoveSupplier { + + private final ArticleRepository articleRepository; + + public RemoveSupplier(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(RemoveSupplierCommand cmd, ActorId performedBy) { + var articleId = ArticleId.of(cmd.articleId()); + + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + article.removeSupplierReference(SupplierId.of(cmd.supplierId())); + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/SetFrameContract.java b/backend/src/main/java/de/effigenix/application/masterdata/SetFrameContract.java new file mode 100644 index 0000000..49b301b --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/SetFrameContract.java @@ -0,0 +1,65 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.SetFrameContractCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.stream.Collectors; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class SetFrameContract { + + private final CustomerRepository customerRepository; + + public SetFrameContract(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(SetFrameContractCommand cmd, ActorId performedBy) { + var customerId = CustomerId.of(cmd.customerId()); + + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + + var lineItems = cmd.lineItems().stream() + .map(li -> new ContractLineItem( + ArticleId.of(li.articleId()), + Money.euro(li.agreedPrice()), + li.agreedQuantity(), + li.unit())) + .collect(Collectors.toList()); + + try { + var contract = FrameContract.create(cmd.validFrom(), cmd.validUntil(), cmd.rhythm(), lineItems); + var setResult = customer.setFrameContract(contract); + if (setResult.isFailure()) { + return Result.failure(setResult.unsafeGetError()); + } + } catch (IllegalArgumentException e) { + return Result.failure(new CustomerError.InvalidFrameContract(e.getMessage())); + } + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/SetPreferences.java b/backend/src/main/java/de/effigenix/application/masterdata/SetPreferences.java new file mode 100644 index 0000000..68c7cd3 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/SetPreferences.java @@ -0,0 +1,46 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.SetPreferencesCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class SetPreferences { + + private final CustomerRepository customerRepository; + + public SetPreferences(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(SetPreferencesCommand cmd, ActorId performedBy) { + var customerId = CustomerId.of(cmd.customerId()); + + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + customer.setPreferences(cmd.preferences()); + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/UpdateArticle.java b/backend/src/main/java/de/effigenix/application/masterdata/UpdateArticle.java new file mode 100644 index 0000000..62415ac --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/UpdateArticle.java @@ -0,0 +1,52 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.UpdateArticleCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class UpdateArticle { + + private final ArticleRepository articleRepository; + + public UpdateArticle(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(UpdateArticleCommand cmd, ActorId performedBy) { + var articleId = ArticleId.of(cmd.articleId()); + + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + + if (cmd.name() != null) { + article.rename(new ArticleName(cmd.name())); + } + if (cmd.categoryId() != null) { + article.changeCategory(ProductCategoryId.of(cmd.categoryId())); + } + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/UpdateCustomer.java b/backend/src/main/java/de/effigenix/application/masterdata/UpdateCustomer.java new file mode 100644 index 0000000..0768ebf --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/UpdateCustomer.java @@ -0,0 +1,51 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.UpdateCustomerCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.*; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class UpdateCustomer { + + private final CustomerRepository customerRepository; + + public UpdateCustomer(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public Result execute(UpdateCustomerCommand cmd, ActorId performedBy) { + var customerId = CustomerId.of(cmd.customerId()); + + switch (customerRepository.findById(customerId)) { + case Failure> f -> + { return Result.failure(new CustomerError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new CustomerError.CustomerNotFound(customerId)); + } + Customer customer = s.value().get(); + + if (cmd.name() != null) { + customer.updateName(new CustomerName(cmd.name())); + } + customer.updateBillingAddress(new Address(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country())); + customer.updateContactInfo(new ContactInfo(cmd.phone(), cmd.email(), cmd.contactPerson())); + customer.updatePaymentTerms(new PaymentTerms(cmd.paymentDueDays(), cmd.paymentDescription())); + + switch (customerRepository.save(customer)) { + case Failure f2 -> + { return Result.failure(new CustomerError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(customer); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/UpdateProductCategory.java b/backend/src/main/java/de/effigenix/application/masterdata/UpdateProductCategory.java new file mode 100644 index 0000000..c3155d3 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/UpdateProductCategory.java @@ -0,0 +1,52 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.UpdateProductCategoryCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class UpdateProductCategory { + + private final ProductCategoryRepository categoryRepository; + + public UpdateProductCategory(ProductCategoryRepository categoryRepository) { + this.categoryRepository = categoryRepository; + } + + public Result execute(UpdateProductCategoryCommand cmd, ActorId performedBy) { + var categoryId = ProductCategoryId.of(cmd.categoryId()); + + switch (categoryRepository.findById(categoryId)) { + case Failure> f -> + { return Result.failure(new ProductCategoryError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ProductCategoryError.CategoryNotFound(categoryId)); + } + ProductCategory category = s.value().get(); + + if (cmd.name() != null) { + category.rename(new CategoryName(cmd.name())); + } + if (cmd.description() != null) { + category.updateDescription(cmd.description()); + } + + switch (categoryRepository.save(category)) { + case Failure f2 -> + { return Result.failure(new ProductCategoryError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(category); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/UpdateSalesUnitPrice.java b/backend/src/main/java/de/effigenix/application/masterdata/UpdateSalesUnitPrice.java new file mode 100644 index 0000000..037cce6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/UpdateSalesUnitPrice.java @@ -0,0 +1,52 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.UpdateSalesUnitPriceCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class UpdateSalesUnitPrice { + + private final ArticleRepository articleRepository; + + public UpdateSalesUnitPrice(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Result execute(UpdateSalesUnitPriceCommand cmd, ActorId performedBy) { + var articleId = ArticleId.of(cmd.articleId()); + + switch (articleRepository.findById(articleId)) { + case Failure> f -> + { return Result.failure(new ArticleError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new ArticleError.ArticleNotFound(articleId)); + } + Article article = s.value().get(); + + var updateResult = article.updateSalesUnitPrice( + SalesUnitId.of(cmd.salesUnitId()), Money.euro(cmd.price())); + if (updateResult.isFailure()) { + return Result.failure(updateResult.unsafeGetError()); + } + + switch (articleRepository.save(article)) { + case Failure f2 -> + { return Result.failure(new ArticleError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(article); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/UpdateSupplier.java b/backend/src/main/java/de/effigenix/application/masterdata/UpdateSupplier.java new file mode 100644 index 0000000..221a6b7 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/UpdateSupplier.java @@ -0,0 +1,51 @@ +package de.effigenix.application.masterdata; + +import de.effigenix.application.masterdata.command.UpdateSupplierCommand; +import de.effigenix.domain.masterdata.*; +import de.effigenix.shared.common.*; +import de.effigenix.shared.security.ActorId; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static de.effigenix.shared.common.Result.*; + +@Transactional +public class UpdateSupplier { + + private final SupplierRepository supplierRepository; + + public UpdateSupplier(SupplierRepository supplierRepository) { + this.supplierRepository = supplierRepository; + } + + public Result execute(UpdateSupplierCommand cmd, ActorId performedBy) { + var supplierId = SupplierId.of(cmd.supplierId()); + + switch (supplierRepository.findById(supplierId)) { + case Failure> f -> + { return Result.failure(new SupplierError.RepositoryFailure(f.error().message())); } + case Success> s -> { + if (s.value().isEmpty()) { + return Result.failure(new SupplierError.SupplierNotFound(supplierId)); + } + Supplier supplier = s.value().get(); + + if (cmd.name() != null) { + supplier.updateName(new SupplierName(cmd.name())); + } + supplier.updateAddress(new Address(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country())); + supplier.updateContactInfo(new ContactInfo(cmd.phone(), cmd.email(), cmd.contactPerson())); + supplier.updatePaymentTerms(new PaymentTerms(cmd.paymentDueDays(), cmd.paymentDescription())); + + switch (supplierRepository.save(supplier)) { + case Failure f2 -> + { return Result.failure(new SupplierError.RepositoryFailure(f2.error().message())); } + case Success ignored -> { } + } + + return Result.success(supplier); + } + } + } +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/AddCertificateCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/AddCertificateCommand.java new file mode 100644 index 0000000..c360f5e --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/AddCertificateCommand.java @@ -0,0 +1,11 @@ +package de.effigenix.application.masterdata.command; + +import java.time.LocalDate; + +public record AddCertificateCommand( + String supplierId, + String certificateType, + String issuer, + LocalDate validFrom, + LocalDate validUntil +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/AddDeliveryAddressCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/AddDeliveryAddressCommand.java new file mode 100644 index 0000000..5073c55 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/AddDeliveryAddressCommand.java @@ -0,0 +1,13 @@ +package de.effigenix.application.masterdata.command; + +public record AddDeliveryAddressCommand( + String customerId, + String label, + String street, + String houseNumber, + String postalCode, + String city, + String country, + String contactPerson, + String deliveryNotes +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/AddSalesUnitCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/AddSalesUnitCommand.java new file mode 100644 index 0000000..cf60d11 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/AddSalesUnitCommand.java @@ -0,0 +1,13 @@ +package de.effigenix.application.masterdata.command; + +import de.effigenix.domain.masterdata.PriceModel; +import de.effigenix.domain.masterdata.Unit; + +import java.math.BigDecimal; + +public record AddSalesUnitCommand( + String articleId, + Unit unit, + PriceModel priceModel, + BigDecimal price +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/AssignSupplierCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/AssignSupplierCommand.java new file mode 100644 index 0000000..edc4946 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/AssignSupplierCommand.java @@ -0,0 +1,6 @@ +package de.effigenix.application.masterdata.command; + +public record AssignSupplierCommand( + String articleId, + String supplierId +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/CreateArticleCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateArticleCommand.java new file mode 100644 index 0000000..cc33c2e --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateArticleCommand.java @@ -0,0 +1,15 @@ +package de.effigenix.application.masterdata.command; + +import de.effigenix.domain.masterdata.PriceModel; +import de.effigenix.domain.masterdata.Unit; + +import java.math.BigDecimal; + +public record CreateArticleCommand( + String name, + String articleNumber, + String categoryId, + Unit unit, + PriceModel priceModel, + BigDecimal price +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/CreateCustomerCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateCustomerCommand.java new file mode 100644 index 0000000..7ab4d13 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateCustomerCommand.java @@ -0,0 +1,18 @@ +package de.effigenix.application.masterdata.command; + +import de.effigenix.domain.masterdata.CustomerType; + +public record CreateCustomerCommand( + String name, + CustomerType type, + String street, + String houseNumber, + String postalCode, + String city, + String country, + String phone, + String email, + String contactPerson, + int paymentDueDays, + String paymentDescription +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/CreateProductCategoryCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateProductCategoryCommand.java new file mode 100644 index 0000000..2541f89 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateProductCategoryCommand.java @@ -0,0 +1,6 @@ +package de.effigenix.application.masterdata.command; + +public record CreateProductCategoryCommand( + String name, + String description +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/CreateSupplierCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateSupplierCommand.java new file mode 100644 index 0000000..0c9e394 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/CreateSupplierCommand.java @@ -0,0 +1,15 @@ +package de.effigenix.application.masterdata.command; + +public record CreateSupplierCommand( + String name, + String street, + String houseNumber, + String postalCode, + String city, + String country, + String phone, + String email, + String contactPerson, + int paymentDueDays, + String paymentDescription +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/RateSupplierCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/RateSupplierCommand.java new file mode 100644 index 0000000..3e574a4 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/RateSupplierCommand.java @@ -0,0 +1,8 @@ +package de.effigenix.application.masterdata.command; + +public record RateSupplierCommand( + String supplierId, + int qualityScore, + int deliveryScore, + int priceScore +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveCertificateCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveCertificateCommand.java new file mode 100644 index 0000000..d51c4de --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveCertificateCommand.java @@ -0,0 +1,10 @@ +package de.effigenix.application.masterdata.command; + +import java.time.LocalDate; + +public record RemoveCertificateCommand( + String supplierId, + String certificateType, + String issuer, + LocalDate validFrom +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveDeliveryAddressCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveDeliveryAddressCommand.java new file mode 100644 index 0000000..bad8af6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveDeliveryAddressCommand.java @@ -0,0 +1,6 @@ +package de.effigenix.application.masterdata.command; + +public record RemoveDeliveryAddressCommand( + String customerId, + String label +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSalesUnitCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSalesUnitCommand.java new file mode 100644 index 0000000..858503d --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSalesUnitCommand.java @@ -0,0 +1,6 @@ +package de.effigenix.application.masterdata.command; + +public record RemoveSalesUnitCommand( + String articleId, + String salesUnitId +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSupplierCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSupplierCommand.java new file mode 100644 index 0000000..0bb5da6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/RemoveSupplierCommand.java @@ -0,0 +1,6 @@ +package de.effigenix.application.masterdata.command; + +public record RemoveSupplierCommand( + String articleId, + String supplierId +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/SetFrameContractCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/SetFrameContractCommand.java new file mode 100644 index 0000000..d8c0ccd --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/SetFrameContractCommand.java @@ -0,0 +1,23 @@ +package de.effigenix.application.masterdata.command; + +import de.effigenix.domain.masterdata.DeliveryRhythm; +import de.effigenix.domain.masterdata.Unit; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +public record SetFrameContractCommand( + String customerId, + LocalDate validFrom, + LocalDate validUntil, + DeliveryRhythm rhythm, + List lineItems +) { + public record LineItem( + String articleId, + BigDecimal agreedPrice, + BigDecimal agreedQuantity, + Unit unit + ) {} +} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/SetPreferencesCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/SetPreferencesCommand.java new file mode 100644 index 0000000..004c0a6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/SetPreferencesCommand.java @@ -0,0 +1,10 @@ +package de.effigenix.application.masterdata.command; + +import de.effigenix.domain.masterdata.CustomerPreference; + +import java.util.Set; + +public record SetPreferencesCommand( + String customerId, + Set preferences +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateArticleCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateArticleCommand.java new file mode 100644 index 0000000..4f80dc8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateArticleCommand.java @@ -0,0 +1,7 @@ +package de.effigenix.application.masterdata.command; + +public record UpdateArticleCommand( + String articleId, + String name, + String categoryId +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateCustomerCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateCustomerCommand.java new file mode 100644 index 0000000..43746b5 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateCustomerCommand.java @@ -0,0 +1,16 @@ +package de.effigenix.application.masterdata.command; + +public record UpdateCustomerCommand( + String customerId, + String name, + String street, + String houseNumber, + String postalCode, + String city, + String country, + String phone, + String email, + String contactPerson, + int paymentDueDays, + String paymentDescription +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateProductCategoryCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateProductCategoryCommand.java new file mode 100644 index 0000000..3914a00 --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateProductCategoryCommand.java @@ -0,0 +1,7 @@ +package de.effigenix.application.masterdata.command; + +public record UpdateProductCategoryCommand( + String categoryId, + String name, + String description +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSalesUnitPriceCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSalesUnitPriceCommand.java new file mode 100644 index 0000000..93f7ebd --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSalesUnitPriceCommand.java @@ -0,0 +1,9 @@ +package de.effigenix.application.masterdata.command; + +import java.math.BigDecimal; + +public record UpdateSalesUnitPriceCommand( + String articleId, + String salesUnitId, + BigDecimal price +) {} diff --git a/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSupplierCommand.java b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSupplierCommand.java new file mode 100644 index 0000000..409e1fa --- /dev/null +++ b/backend/src/main/java/de/effigenix/application/masterdata/command/UpdateSupplierCommand.java @@ -0,0 +1,16 @@ +package de.effigenix.application.masterdata.command; + +public record UpdateSupplierCommand( + String supplierId, + String name, + String street, + String houseNumber, + String postalCode, + String city, + String country, + String phone, + String email, + String contactPerson, + int paymentDueDays, + String paymentDescription +) {} diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/AssignRole.java b/backend/src/main/java/de/effigenix/application/usermanagement/AssignRole.java index 0b149a3..1d01660 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/AssignRole.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/AssignRole.java @@ -3,6 +3,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.command.AssignRoleCommand; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/AuthenticateUser.java b/backend/src/main/java/de/effigenix/application/usermanagement/AuthenticateUser.java index 426b2d6..4aa8ac6 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/AuthenticateUser.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/AuthenticateUser.java @@ -3,6 +3,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.command.AuthenticateCommand; import de.effigenix.application.usermanagement.dto.SessionToken; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/ChangePassword.java b/backend/src/main/java/de/effigenix/application/usermanagement/ChangePassword.java index 017df6f..5ffa6d7 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/ChangePassword.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/ChangePassword.java @@ -2,6 +2,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.command.ChangePasswordCommand; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/CreateUser.java b/backend/src/main/java/de/effigenix/application/usermanagement/CreateUser.java index 1312f28..570b0fa 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/CreateUser.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/CreateUser.java @@ -3,6 +3,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.command.CreateUserCommand; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/GetUser.java b/backend/src/main/java/de/effigenix/application/usermanagement/GetUser.java index b0aa5d8..45056ab 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/GetUser.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/GetUser.java @@ -2,6 +2,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java b/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java index 4ec7f2f..aefd249 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/ListUsers.java @@ -1,7 +1,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.dto.UserDTO; -import de.effigenix.domain.usermanagement.RepositoryError; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.domain.usermanagement.User; import de.effigenix.domain.usermanagement.UserError; import de.effigenix.domain.usermanagement.UserRepository; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/LockUser.java b/backend/src/main/java/de/effigenix/application/usermanagement/LockUser.java index b32d6bd..a897054 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/LockUser.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/LockUser.java @@ -2,6 +2,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/RemoveRole.java b/backend/src/main/java/de/effigenix/application/usermanagement/RemoveRole.java index 3123300..9fd25b4 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/RemoveRole.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/RemoveRole.java @@ -2,6 +2,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/UnlockUser.java b/backend/src/main/java/de/effigenix/application/usermanagement/UnlockUser.java index 6d2965d..5d866f8 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/UnlockUser.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/UnlockUser.java @@ -2,6 +2,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/application/usermanagement/UpdateUser.java b/backend/src/main/java/de/effigenix/application/usermanagement/UpdateUser.java index 5337fff..ec9b848 100644 --- a/backend/src/main/java/de/effigenix/application/usermanagement/UpdateUser.java +++ b/backend/src/main/java/de/effigenix/application/usermanagement/UpdateUser.java @@ -3,6 +3,7 @@ package de.effigenix.application.usermanagement; import de.effigenix.application.usermanagement.command.UpdateUserCommand; import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.domain.usermanagement.*; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import de.effigenix.shared.security.ActorId; import org.springframework.transaction.annotation.Transactional; diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/Article.java b/backend/src/main/java/de/effigenix/domain/masterdata/Article.java new file mode 100644 index 0000000..f1da90e --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/Article.java @@ -0,0 +1,204 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.Result; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * Aggregate Root for Article. + * + * Invariants: + * 1. At least one SalesUnit + * 2. No duplicate Unit types + * 3. Unit/PriceModel consistency (enforced by SalesUnit) + * 4. ArticleNumber uniqueness (enforced at Repository level) + */ +public class Article { + + private final ArticleId id; + private ArticleName name; + private final ArticleNumber articleNumber; + private ProductCategoryId categoryId; + private final List salesUnits; + private ArticleStatus status; + private final Set supplierReferences; + private final LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private Article( + ArticleId id, + ArticleName name, + ArticleNumber articleNumber, + ProductCategoryId categoryId, + List salesUnits, + ArticleStatus status, + Set supplierReferences, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + this.id = id; + this.name = name; + this.articleNumber = articleNumber; + this.categoryId = categoryId; + this.salesUnits = new ArrayList<>(salesUnits); + this.status = status; + this.supplierReferences = new HashSet<>(supplierReferences); + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public static Result create( + ArticleName name, + ArticleNumber articleNumber, + ProductCategoryId categoryId, + SalesUnit initialSalesUnit + ) { + if (name == null || articleNumber == null || categoryId == null || initialSalesUnit == null) { + return Result.failure(new ArticleError.MinimumSalesUnitRequired()); + } + var now = LocalDateTime.now(); + return Result.success(new Article( + ArticleId.generate(), + name, + articleNumber, + categoryId, + List.of(initialSalesUnit), + ArticleStatus.ACTIVE, + Set.of(), + now, + now + )); + } + + public static Article reconstitute( + ArticleId id, + ArticleName name, + ArticleNumber articleNumber, + ProductCategoryId categoryId, + List salesUnits, + ArticleStatus status, + Set supplierReferences, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + return new Article(id, name, articleNumber, categoryId, salesUnits, status, + supplierReferences, createdAt, updatedAt); + } + + // ==================== Sales Unit Management ==================== + + public Result addSalesUnit(SalesUnit salesUnit) { + if (hasUnitType(salesUnit.unit())) { + return Result.failure(new ArticleError.DuplicateSalesUnitType(salesUnit.unit())); + } + this.salesUnits.add(salesUnit); + touch(); + return Result.success(null); + } + + public Result removeSalesUnit(SalesUnitId salesUnitId) { + if (this.salesUnits.size() <= 1) { + return Result.failure(new ArticleError.MinimumSalesUnitRequired()); + } + boolean removed = this.salesUnits.removeIf(su -> su.id().equals(salesUnitId)); + if (!removed) { + return Result.failure(new ArticleError.SalesUnitNotFound(salesUnitId)); + } + touch(); + return Result.success(null); + } + + public Result updateSalesUnitPrice(SalesUnitId salesUnitId, Money newPrice) { + var salesUnit = findSalesUnit(salesUnitId); + if (salesUnit == null) { + return Result.failure(new ArticleError.SalesUnitNotFound(salesUnitId)); + } + var result = salesUnit.updatePrice(newPrice); + if (result.isSuccess()) { + touch(); + } + return result; + } + + // ==================== Article Properties ==================== + + public void rename(ArticleName newName) { + this.name = newName; + touch(); + } + + public void changeCategory(ProductCategoryId newCategoryId) { + this.categoryId = newCategoryId; + touch(); + } + + public void activate() { + this.status = ArticleStatus.ACTIVE; + touch(); + } + + public void deactivate() { + this.status = ArticleStatus.INACTIVE; + touch(); + } + + // ==================== Supplier References ==================== + + public void addSupplierReference(SupplierId supplierId) { + this.supplierReferences.add(supplierId); + touch(); + } + + public void removeSupplierReference(SupplierId supplierId) { + this.supplierReferences.remove(supplierId); + touch(); + } + + // ==================== Helpers ==================== + + private boolean hasUnitType(Unit unit) { + return salesUnits.stream().anyMatch(su -> su.unit() == unit); + } + + private SalesUnit findSalesUnit(SalesUnitId id) { + return salesUnits.stream() + .filter(su -> su.id().equals(id)) + .findFirst() + .orElse(null); + } + + private void touch() { + this.updatedAt = LocalDateTime.now(); + } + + // ==================== Getters ==================== + + public ArticleId id() { return id; } + public ArticleName name() { return name; } + public ArticleNumber articleNumber() { return articleNumber; } + public ProductCategoryId categoryId() { return categoryId; } + public List salesUnits() { return Collections.unmodifiableList(salesUnits); } + public ArticleStatus status() { return status; } + public Set supplierReferences() { return Collections.unmodifiableSet(supplierReferences); } + public LocalDateTime createdAt() { return createdAt; } + public LocalDateTime updatedAt() { return updatedAt; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Article other)) return false; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return "Article{id=" + id + ", name=" + name + ", number=" + articleNumber + ", status=" + status + "}"; + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ArticleError.java b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleError.java new file mode 100644 index 0000000..58aa523 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleError.java @@ -0,0 +1,50 @@ +package de.effigenix.domain.masterdata; + +public sealed interface ArticleError { + + String code(); + String message(); + + record ArticleNotFound(ArticleId id) implements ArticleError { + @Override public String code() { return "ARTICLE_NOT_FOUND"; } + @Override public String message() { return "Article with ID '" + id.value() + "' not found"; } + } + + record ArticleNumberAlreadyExists(String articleNumber) implements ArticleError { + @Override public String code() { return "ARTICLE_NUMBER_EXISTS"; } + @Override public String message() { return "Article number '" + articleNumber + "' already exists"; } + } + + record SalesUnitNotFound(SalesUnitId id) implements ArticleError { + @Override public String code() { return "SALES_UNIT_NOT_FOUND"; } + @Override public String message() { return "SalesUnit with ID '" + id.value() + "' not found"; } + } + + record MinimumSalesUnitRequired() implements ArticleError { + @Override public String code() { return "MINIMUM_SALES_UNIT"; } + @Override public String message() { return "Article must have at least one SalesUnit"; } + } + + record DuplicateSalesUnitType(Unit unit) implements ArticleError { + @Override public String code() { return "DUPLICATE_SALES_UNIT_TYPE"; } + @Override public String message() { return "Article already has a SalesUnit of type " + unit; } + } + + record InvalidPriceModelCombination(String reason) implements ArticleError { + @Override public String code() { return "INVALID_PRICE_MODEL"; } + @Override public String message() { return "Invalid Unit/PriceModel combination: " + reason; } + } + + record InvalidPrice(String reason) implements ArticleError { + @Override public String code() { return "INVALID_PRICE"; } + @Override public String message() { return "Invalid price: " + reason; } + } + + record Unauthorized(String message) implements ArticleError { + @Override public String code() { return "UNAUTHORIZED"; } + } + + record RepositoryFailure(String message) implements ArticleError { + @Override public String code() { return "REPOSITORY_ERROR"; } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ArticleId.java b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleId.java new file mode 100644 index 0000000..17d155b --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleId.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import java.util.UUID; + +public record ArticleId(String value) { + + public ArticleId { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("ArticleId must not be blank"); + } + } + + public static ArticleId generate() { + return new ArticleId(UUID.randomUUID().toString()); + } + + public static ArticleId of(String value) { + return new ArticleId(value); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ArticleName.java b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleName.java new file mode 100644 index 0000000..c40e1d6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleName.java @@ -0,0 +1,13 @@ +package de.effigenix.domain.masterdata; + +public record ArticleName(String value) { + + public ArticleName { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("ArticleName must not be blank"); + } + if (value.length() > 200) { + throw new IllegalArgumentException("ArticleName must not exceed 200 characters"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ArticleNumber.java b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleNumber.java new file mode 100644 index 0000000..1d2820a --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleNumber.java @@ -0,0 +1,13 @@ +package de.effigenix.domain.masterdata; + +public record ArticleNumber(String value) { + + public ArticleNumber { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("ArticleNumber must not be blank"); + } + if (value.length() > 50) { + throw new IllegalArgumentException("ArticleNumber must not exceed 50 characters"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ArticleRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleRepository.java new file mode 100644 index 0000000..20c58f2 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleRepository.java @@ -0,0 +1,24 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; + +import java.util.List; +import java.util.Optional; + +public interface ArticleRepository { + + Result> findById(ArticleId id); + + Result> findAll(); + + Result> findByCategory(ProductCategoryId categoryId); + + Result> findByStatus(ArticleStatus status); + + Result save(Article article); + + Result delete(Article article); + + Result existsByArticleNumber(ArticleNumber articleNumber); +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ArticleStatus.java b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleStatus.java new file mode 100644 index 0000000..4361fe5 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ArticleStatus.java @@ -0,0 +1,6 @@ +package de.effigenix.domain.masterdata; + +public enum ArticleStatus { + ACTIVE, + INACTIVE +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CategoryName.java b/backend/src/main/java/de/effigenix/domain/masterdata/CategoryName.java new file mode 100644 index 0000000..773d84e --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CategoryName.java @@ -0,0 +1,13 @@ +package de.effigenix.domain.masterdata; + +public record CategoryName(String value) { + + public CategoryName { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("CategoryName must not be blank"); + } + if (value.length() > 100) { + throw new IllegalArgumentException("CategoryName must not exceed 100 characters"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ContractLineItem.java b/backend/src/main/java/de/effigenix/domain/masterdata/ContractLineItem.java new file mode 100644 index 0000000..f367e44 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ContractLineItem.java @@ -0,0 +1,28 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Money; + +import java.math.BigDecimal; + +/** + * Value Object representing a line item in a frame contract. + */ +public record ContractLineItem( + ArticleId articleId, + Money agreedPrice, + BigDecimal agreedQuantity, + Unit unit +) { + + public ContractLineItem { + if (articleId == null) { + throw new IllegalArgumentException("ArticleId must not be null"); + } + if (agreedPrice == null) { + throw new IllegalArgumentException("Agreed price must not be null"); + } + if (agreedQuantity != null && agreedQuantity.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Agreed quantity must be positive when set"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/Customer.java b/backend/src/main/java/de/effigenix/domain/masterdata/Customer.java new file mode 100644 index 0000000..ce07a20 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/Customer.java @@ -0,0 +1,225 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Address; +import de.effigenix.shared.common.ContactInfo; +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.PaymentTerms; +import de.effigenix.shared.common.Result; + +import java.time.LocalDateTime; +import java.util.*; + +/** + * Aggregate Root for Customer. + * + * Invariants: + * 1. Name, CustomerType, billingAddress non-null + * 2. FrameContract only for B2B customers + * 3. No duplicate ContractLineItems per ArticleId (enforced by FrameContract) + */ +public class Customer { + + private final CustomerId id; + private CustomerName name; + private final CustomerType type; + private Address billingAddress; + private ContactInfo contactInfo; + private PaymentTerms paymentTerms; + private final List deliveryAddresses; + private FrameContract frameContract; + private final Set preferences; + private CustomerStatus status; + private final LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private Customer( + CustomerId id, + CustomerName name, + CustomerType type, + Address billingAddress, + ContactInfo contactInfo, + PaymentTerms paymentTerms, + List deliveryAddresses, + FrameContract frameContract, + Set preferences, + CustomerStatus status, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + this.id = id; + this.name = name; + this.type = type; + this.billingAddress = billingAddress; + this.contactInfo = contactInfo; + this.paymentTerms = paymentTerms; + this.deliveryAddresses = new ArrayList<>(deliveryAddresses); + this.frameContract = frameContract; + this.preferences = new HashSet<>(preferences); + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public static Result create( + CustomerName name, + CustomerType type, + Address billingAddress, + ContactInfo contactInfo, + PaymentTerms paymentTerms + ) { + if (name == null || type == null || billingAddress == null) { + return Result.failure(new CustomerError.CustomerNotFound(CustomerId.of("N/A"))); + } + var now = LocalDateTime.now(); + return Result.success(new Customer( + CustomerId.generate(), name, type, billingAddress, contactInfo, paymentTerms, + List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now + )); + } + + public static Customer reconstitute( + CustomerId id, + CustomerName name, + CustomerType type, + Address billingAddress, + ContactInfo contactInfo, + PaymentTerms paymentTerms, + List deliveryAddresses, + FrameContract frameContract, + Set preferences, + CustomerStatus status, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + return new Customer(id, name, type, billingAddress, contactInfo, paymentTerms, + deliveryAddresses, frameContract, preferences, status, createdAt, updatedAt); + } + + // ==================== Business Methods ==================== + + public void updateName(CustomerName newName) { + this.name = newName; + touch(); + } + + public void updateBillingAddress(Address newAddress) { + this.billingAddress = newAddress; + touch(); + } + + public void updateContactInfo(ContactInfo newContactInfo) { + this.contactInfo = newContactInfo; + touch(); + } + + public void updatePaymentTerms(PaymentTerms newPaymentTerms) { + this.paymentTerms = newPaymentTerms; + touch(); + } + + // ==================== Delivery Addresses ==================== + + public void addDeliveryAddress(DeliveryAddress address) { + this.deliveryAddresses.add(address); + touch(); + } + + public void removeDeliveryAddress(String label) { + this.deliveryAddresses.removeIf(da -> + Objects.equals(da.label(), label)); + touch(); + } + + // ==================== Frame Contract ==================== + + public Result setFrameContract(FrameContract contract) { + if (this.type != CustomerType.B2B) { + return Result.failure(new CustomerError.FrameContractNotAllowed()); + } + this.frameContract = contract; + touch(); + return Result.success(null); + } + + public void removeFrameContract() { + this.frameContract = null; + touch(); + } + + public boolean hasActiveFrameContract() { + return frameContract != null && frameContract.isActive(); + } + + public Optional getAgreedPrice(ArticleId articleId) { + if (frameContract == null) return Optional.empty(); + return frameContract.getAgreedPrice(articleId); + } + + // ==================== Preferences ==================== + + public void addPreference(CustomerPreference preference) { + this.preferences.add(preference); + touch(); + } + + public void removePreference(CustomerPreference preference) { + this.preferences.remove(preference); + touch(); + } + + public void setPreferences(Set newPreferences) { + this.preferences.clear(); + this.preferences.addAll(newPreferences); + touch(); + } + + // ==================== Status ==================== + + public void activate() { + this.status = CustomerStatus.ACTIVE; + touch(); + } + + public void deactivate() { + this.status = CustomerStatus.INACTIVE; + touch(); + } + + // ==================== Helpers ==================== + + private void touch() { + this.updatedAt = LocalDateTime.now(); + } + + // ==================== Getters ==================== + + public CustomerId id() { return id; } + public CustomerName name() { return name; } + public CustomerType type() { return type; } + public Address billingAddress() { return billingAddress; } + public ContactInfo contactInfo() { return contactInfo; } + public PaymentTerms paymentTerms() { return paymentTerms; } + public List deliveryAddresses() { return Collections.unmodifiableList(deliveryAddresses); } + public FrameContract frameContract() { return frameContract; } + public Set preferences() { return Collections.unmodifiableSet(preferences); } + public CustomerStatus status() { return status; } + public LocalDateTime createdAt() { return createdAt; } + public LocalDateTime updatedAt() { return updatedAt; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Customer other)) return false; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return "Customer{id=" + id + ", name=" + name + ", type=" + type + ", status=" + status + "}"; + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CustomerError.java b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerError.java new file mode 100644 index 0000000..da58701 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerError.java @@ -0,0 +1,40 @@ +package de.effigenix.domain.masterdata; + +public sealed interface CustomerError { + + String code(); + String message(); + + record CustomerNotFound(CustomerId id) implements CustomerError { + @Override public String code() { return "CUSTOMER_NOT_FOUND"; } + @Override public String message() { return "Customer with ID '" + id.value() + "' not found"; } + } + + record CustomerNameAlreadyExists(String name) implements CustomerError { + @Override public String code() { return "CUSTOMER_NAME_EXISTS"; } + @Override public String message() { return "Customer name '" + name + "' already exists"; } + } + + record FrameContractNotAllowed() implements CustomerError { + @Override public String code() { return "FRAME_CONTRACT_NOT_ALLOWED"; } + @Override public String message() { return "Frame contracts are only allowed for B2B customers"; } + } + + record InvalidFrameContract(String reason) implements CustomerError { + @Override public String code() { return "INVALID_FRAME_CONTRACT"; } + @Override public String message() { return "Invalid frame contract: " + reason; } + } + + record DuplicateContractLineItem(ArticleId articleId) implements CustomerError { + @Override public String code() { return "DUPLICATE_LINE_ITEM"; } + @Override public String message() { return "Duplicate line item for article '" + articleId.value() + "'"; } + } + + record Unauthorized(String message) implements CustomerError { + @Override public String code() { return "UNAUTHORIZED"; } + } + + record RepositoryFailure(String message) implements CustomerError { + @Override public String code() { return "REPOSITORY_ERROR"; } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CustomerId.java b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerId.java new file mode 100644 index 0000000..3d15aa1 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerId.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import java.util.UUID; + +public record CustomerId(String value) { + + public CustomerId { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("CustomerId must not be blank"); + } + } + + public static CustomerId generate() { + return new CustomerId(UUID.randomUUID().toString()); + } + + public static CustomerId of(String value) { + return new CustomerId(value); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CustomerName.java b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerName.java new file mode 100644 index 0000000..b27ab36 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerName.java @@ -0,0 +1,13 @@ +package de.effigenix.domain.masterdata; + +public record CustomerName(String value) { + + public CustomerName { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("CustomerName must not be blank"); + } + if (value.length() > 200) { + throw new IllegalArgumentException("CustomerName must not exceed 200 characters"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CustomerPreference.java b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerPreference.java new file mode 100644 index 0000000..61cba9b --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerPreference.java @@ -0,0 +1,11 @@ +package de.effigenix.domain.masterdata; + +public enum CustomerPreference { + BIO, + REGIONAL, + TIERWOHL, + HALAL, + KOSHER, + GLUTENFREI, + LAKTOSEFREI +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CustomerRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerRepository.java new file mode 100644 index 0000000..9f1f6bf --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerRepository.java @@ -0,0 +1,24 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; + +import java.util.List; +import java.util.Optional; + +public interface CustomerRepository { + + Result> findById(CustomerId id); + + Result> findAll(); + + Result> findByType(CustomerType type); + + Result> findByStatus(CustomerStatus status); + + Result save(Customer customer); + + Result delete(Customer customer); + + Result existsByName(CustomerName name); +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CustomerStatus.java b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerStatus.java new file mode 100644 index 0000000..f0197f8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerStatus.java @@ -0,0 +1,6 @@ +package de.effigenix.domain.masterdata; + +public enum CustomerStatus { + ACTIVE, + INACTIVE +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/CustomerType.java b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerType.java new file mode 100644 index 0000000..3d44b49 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/CustomerType.java @@ -0,0 +1,6 @@ +package de.effigenix.domain.masterdata; + +public enum CustomerType { + B2C, + B2B +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/DeliveryAddress.java b/backend/src/main/java/de/effigenix/domain/masterdata/DeliveryAddress.java new file mode 100644 index 0000000..ec2e93a --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/DeliveryAddress.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Address; + +/** + * Value Object representing a labeled delivery address. + */ +public record DeliveryAddress( + String label, + Address address, + String contactPerson, + String deliveryNotes +) { + + public DeliveryAddress { + if (address == null) { + throw new IllegalArgumentException("Address must not be null"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/DeliveryRhythm.java b/backend/src/main/java/de/effigenix/domain/masterdata/DeliveryRhythm.java new file mode 100644 index 0000000..b1fe296 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/DeliveryRhythm.java @@ -0,0 +1,9 @@ +package de.effigenix.domain.masterdata; + +public enum DeliveryRhythm { + DAILY, + WEEKLY, + BIWEEKLY, + MONTHLY, + ON_DEMAND +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/FrameContract.java b/backend/src/main/java/de/effigenix/domain/masterdata/FrameContract.java new file mode 100644 index 0000000..a6ddcd3 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/FrameContract.java @@ -0,0 +1,111 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Money; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Entity representing a frame contract within a Customer aggregate. + * No own lifecycle — belongs to Customer (only B2B). + * + * Invariants: + * - validUntil >= validFrom + * - No duplicate ArticleIds in lineItems + * - At least one lineItem + */ +public class FrameContract { + + private final FrameContractId id; + private final LocalDate validFrom; + private final LocalDate validUntil; + private final DeliveryRhythm deliveryRhythm; + private final List lineItems; + + private FrameContract( + FrameContractId id, + LocalDate validFrom, + LocalDate validUntil, + DeliveryRhythm deliveryRhythm, + List lineItems + ) { + this.id = id; + this.validFrom = validFrom; + this.validUntil = validUntil; + this.deliveryRhythm = deliveryRhythm; + this.lineItems = new ArrayList<>(lineItems); + } + + public static FrameContract create( + LocalDate validFrom, + LocalDate validUntil, + DeliveryRhythm deliveryRhythm, + List lineItems + ) { + if (validFrom != null && validUntil != null && validUntil.isBefore(validFrom)) { + throw new IllegalArgumentException("validUntil must not be before validFrom"); + } + if (lineItems == null || lineItems.isEmpty()) { + throw new IllegalArgumentException("FrameContract must have at least one line item"); + } + long distinctArticles = lineItems.stream() + .map(ContractLineItem::articleId) + .distinct() + .count(); + if (distinctArticles != lineItems.size()) { + throw new IllegalArgumentException("Duplicate ArticleIds in line items"); + } + return new FrameContract(FrameContractId.generate(), validFrom, validUntil, deliveryRhythm, lineItems); + } + + public static FrameContract reconstitute( + FrameContractId id, + LocalDate validFrom, + LocalDate validUntil, + DeliveryRhythm deliveryRhythm, + List lineItems + ) { + return new FrameContract(id, validFrom, validUntil, deliveryRhythm, lineItems); + } + + public boolean isActive() { + var today = LocalDate.now(); + return (validFrom == null || !today.isBefore(validFrom)) + && (validUntil == null || !today.isAfter(validUntil)); + } + + public boolean isExpired() { + return validUntil != null && LocalDate.now().isAfter(validUntil); + } + + public Optional findLineItem(ArticleId articleId) { + return lineItems.stream() + .filter(li -> li.articleId().equals(articleId)) + .findFirst(); + } + + public Optional getAgreedPrice(ArticleId articleId) { + return findLineItem(articleId).map(ContractLineItem::agreedPrice); + } + + public FrameContractId id() { return id; } + public LocalDate validFrom() { return validFrom; } + public LocalDate validUntil() { return validUntil; } + public DeliveryRhythm deliveryRhythm() { return deliveryRhythm; } + public List lineItems() { return Collections.unmodifiableList(lineItems); } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof FrameContract other)) return false; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/FrameContractId.java b/backend/src/main/java/de/effigenix/domain/masterdata/FrameContractId.java new file mode 100644 index 0000000..dfafd9d --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/FrameContractId.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import java.util.UUID; + +public record FrameContractId(String value) { + + public FrameContractId { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("FrameContractId must not be blank"); + } + } + + public static FrameContractId generate() { + return new FrameContractId(UUID.randomUUID().toString()); + } + + public static FrameContractId of(String value) { + return new FrameContractId(value); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/MasterDataAction.java b/backend/src/main/java/de/effigenix/domain/masterdata/MasterDataAction.java new file mode 100644 index 0000000..c9cddd4 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/MasterDataAction.java @@ -0,0 +1,24 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.security.Action; + +public enum MasterDataAction implements Action { + CREATE_ARTICLE, + UPDATE_ARTICLE, + DELETE_ARTICLE, + VIEW_ARTICLE, + CREATE_SUPPLIER, + UPDATE_SUPPLIER, + DELETE_SUPPLIER, + VIEW_SUPPLIER, + RATE_SUPPLIER, + CREATE_CUSTOMER, + UPDATE_CUSTOMER, + DELETE_CUSTOMER, + VIEW_CUSTOMER, + MANAGE_FRAME_CONTRACT, + CREATE_CATEGORY, + UPDATE_CATEGORY, + DELETE_CATEGORY, + VIEW_CATEGORY +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/PriceModel.java b/backend/src/main/java/de/effigenix/domain/masterdata/PriceModel.java new file mode 100644 index 0000000..ea776d7 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/PriceModel.java @@ -0,0 +1,6 @@ +package de.effigenix.domain.masterdata; + +public enum PriceModel { + FIXED, + WEIGHT_BASED +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategory.java b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategory.java new file mode 100644 index 0000000..5656d82 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategory.java @@ -0,0 +1,60 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Result; + +/** + * Mini-Aggregate for product categories. + * DB-based entity, dynamically extendable by users. + */ +public class ProductCategory { + + private final ProductCategoryId id; + private CategoryName name; + private String description; + + private ProductCategory(ProductCategoryId id, CategoryName name, String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public static Result create(CategoryName name, String description) { + if (name == null) { + return Result.failure(new ProductCategoryError.InvalidCategoryName("Name must not be null")); + } + return Result.success(new ProductCategory(ProductCategoryId.generate(), name, description)); + } + + public static ProductCategory reconstitute(ProductCategoryId id, CategoryName name, String description) { + return new ProductCategory(id, name, description); + } + + public void rename(CategoryName newName) { + this.name = newName; + } + + public void updateDescription(String newDescription) { + this.description = newDescription; + } + + public ProductCategoryId id() { return id; } + public CategoryName name() { return name; } + public String description() { return description; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ProductCategory other)) return false; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return "ProductCategory{id=" + id + ", name=" + name + "}"; + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryError.java b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryError.java new file mode 100644 index 0000000..a3068a3 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryError.java @@ -0,0 +1,35 @@ +package de.effigenix.domain.masterdata; + +public sealed interface ProductCategoryError { + + String code(); + String message(); + + record CategoryNotFound(ProductCategoryId id) implements ProductCategoryError { + @Override public String code() { return "CATEGORY_NOT_FOUND"; } + @Override public String message() { return "ProductCategory with ID '" + id.value() + "' not found"; } + } + + record CategoryNameAlreadyExists(String name) implements ProductCategoryError { + @Override public String code() { return "CATEGORY_NAME_EXISTS"; } + @Override public String message() { return "Category name '" + name + "' already exists"; } + } + + record CategoryInUse(ProductCategoryId id) implements ProductCategoryError { + @Override public String code() { return "CATEGORY_IN_USE"; } + @Override public String message() { return "Category '" + id.value() + "' is still assigned to articles"; } + } + + record InvalidCategoryName(String reason) implements ProductCategoryError { + @Override public String code() { return "CATEGORY_INVALID_NAME"; } + @Override public String message() { return "Invalid category name: " + reason; } + } + + record Unauthorized(String message) implements ProductCategoryError { + @Override public String code() { return "UNAUTHORIZED"; } + } + + record RepositoryFailure(String message) implements ProductCategoryError { + @Override public String code() { return "REPOSITORY_ERROR"; } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryId.java b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryId.java new file mode 100644 index 0000000..22c121c --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryId.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import java.util.UUID; + +public record ProductCategoryId(String value) { + + public ProductCategoryId { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("ProductCategoryId must not be blank"); + } + } + + public static ProductCategoryId generate() { + return new ProductCategoryId(UUID.randomUUID().toString()); + } + + public static ProductCategoryId of(String value) { + return new ProductCategoryId(value); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryRepository.java new file mode 100644 index 0000000..830d7e2 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/ProductCategoryRepository.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; + +import java.util.List; +import java.util.Optional; + +public interface ProductCategoryRepository { + + Result> findById(ProductCategoryId id); + + Result> findAll(); + + Result save(ProductCategory category); + + Result delete(ProductCategory category); + + Result existsByName(CategoryName name); +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/QualityCertificate.java b/backend/src/main/java/de/effigenix/domain/masterdata/QualityCertificate.java new file mode 100644 index 0000000..b37b0f8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/QualityCertificate.java @@ -0,0 +1,33 @@ +package de.effigenix.domain.masterdata; + +import java.time.LocalDate; + +/** + * Value Object representing a quality certificate held by a supplier. + * Immutable, identified by content (record equality). + */ +public record QualityCertificate( + String certificateType, + String issuer, + LocalDate validFrom, + LocalDate validUntil +) { + + public QualityCertificate { + if (certificateType == null || certificateType.isBlank()) { + throw new IllegalArgumentException("Certificate type must not be blank"); + } + if (validFrom != null && validUntil != null && validUntil.isBefore(validFrom)) { + throw new IllegalArgumentException("validUntil must not be before validFrom"); + } + } + + public boolean isExpired() { + return validUntil != null && validUntil.isBefore(LocalDate.now()); + } + + public boolean expiresWithinDays(int days) { + if (validUntil == null) return false; + return !isExpired() && validUntil.isBefore(LocalDate.now().plusDays(days)); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SalesUnit.java b/backend/src/main/java/de/effigenix/domain/masterdata/SalesUnit.java new file mode 100644 index 0000000..374977f --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SalesUnit.java @@ -0,0 +1,77 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Money; +import de.effigenix.shared.common.Result; + +import java.math.BigDecimal; + +/** + * Entity representing a sales unit within an Article. + * Enforces Unit/PriceModel consistency: + * PIECE_FIXED → FIXED; KG/HUNDRED_GRAM/PIECE_VARIABLE → WEIGHT_BASED + */ +public class SalesUnit { + + private final SalesUnitId id; + private final Unit unit; + private final PriceModel priceModel; + private Money price; + + private SalesUnit(SalesUnitId id, Unit unit, PriceModel priceModel, Money price) { + this.id = id; + this.unit = unit; + this.priceModel = priceModel; + this.price = price; + } + + public static Result create(Unit unit, PriceModel priceModel, Money price) { + if (unit == null || priceModel == null) { + return Result.failure(new ArticleError.InvalidPriceModelCombination( + "Unit and PriceModel must not be null")); + } + if (!isValidCombination(unit, priceModel)) { + return Result.failure(new ArticleError.InvalidPriceModelCombination( + "Unit " + unit + " is not compatible with PriceModel " + priceModel)); + } + if (price == null || price.amount().compareTo(BigDecimal.ZERO) <= 0) { + return Result.failure(new ArticleError.InvalidPrice("Price must be positive")); + } + return Result.success(new SalesUnit(SalesUnitId.generate(), unit, priceModel, price)); + } + + public static SalesUnit reconstitute(SalesUnitId id, Unit unit, PriceModel priceModel, Money price) { + return new SalesUnit(id, unit, priceModel, price); + } + + public Result updatePrice(Money newPrice) { + if (newPrice == null || newPrice.amount().compareTo(BigDecimal.ZERO) <= 0) { + return Result.failure(new ArticleError.InvalidPrice("Price must be positive")); + } + this.price = newPrice; + return Result.success(null); + } + + private static boolean isValidCombination(Unit unit, PriceModel priceModel) { + return switch (unit) { + case PIECE_FIXED -> priceModel == PriceModel.FIXED; + case KG, HUNDRED_GRAM, PIECE_VARIABLE -> priceModel == PriceModel.WEIGHT_BASED; + }; + } + + public SalesUnitId id() { return id; } + public Unit unit() { return unit; } + public PriceModel priceModel() { return priceModel; } + public Money price() { return price; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof SalesUnit other)) return false; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SalesUnitId.java b/backend/src/main/java/de/effigenix/domain/masterdata/SalesUnitId.java new file mode 100644 index 0000000..c165340 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SalesUnitId.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import java.util.UUID; + +public record SalesUnitId(String value) { + + public SalesUnitId { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("SalesUnitId must not be blank"); + } + } + + public static SalesUnitId generate() { + return new SalesUnitId(UUID.randomUUID().toString()); + } + + public static SalesUnitId of(String value) { + return new SalesUnitId(value); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/Supplier.java b/backend/src/main/java/de/effigenix/domain/masterdata/Supplier.java new file mode 100644 index 0000000..e84d34b --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/Supplier.java @@ -0,0 +1,186 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.Address; +import de.effigenix.shared.common.ContactInfo; +import de.effigenix.shared.common.PaymentTerms; +import de.effigenix.shared.common.Result; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Aggregate Root for Supplier. + * + * Invariants: + * 1. Name, Address, PaymentTerms non-null + * 2. No structurally identical certificates (record equality) + * 3. Rating scores 1-5 (enforced by SupplierRating VO) + */ +public class Supplier { + + private final SupplierId id; + private SupplierName name; + private Address address; + private ContactInfo contactInfo; + private PaymentTerms paymentTerms; + private final List certificates; + private SupplierRating rating; + private SupplierStatus status; + private final LocalDateTime createdAt; + private LocalDateTime updatedAt; + + private Supplier( + SupplierId id, + SupplierName name, + Address address, + ContactInfo contactInfo, + PaymentTerms paymentTerms, + List certificates, + SupplierRating rating, + SupplierStatus status, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + this.id = id; + this.name = name; + this.address = address; + this.contactInfo = contactInfo; + this.paymentTerms = paymentTerms; + this.certificates = new ArrayList<>(certificates); + this.rating = rating; + this.status = status; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public static Result create( + SupplierName name, + Address address, + ContactInfo contactInfo, + PaymentTerms paymentTerms + ) { + if (name == null || address == null || paymentTerms == null) { + return Result.failure(new SupplierError.SupplierNotFound(SupplierId.of("N/A"))); + } + var now = LocalDateTime.now(); + return Result.success(new Supplier( + SupplierId.generate(), name, address, contactInfo, paymentTerms, + List.of(), null, SupplierStatus.ACTIVE, now, now + )); + } + + public static Supplier reconstitute( + SupplierId id, + SupplierName name, + Address address, + ContactInfo contactInfo, + PaymentTerms paymentTerms, + List certificates, + SupplierRating rating, + SupplierStatus status, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + return new Supplier(id, name, address, contactInfo, paymentTerms, + certificates, rating, status, createdAt, updatedAt); + } + + // ==================== Business Methods ==================== + + public void updateName(SupplierName newName) { + this.name = newName; + touch(); + } + + public void updateAddress(Address newAddress) { + this.address = newAddress; + touch(); + } + + public void updateContactInfo(ContactInfo newContactInfo) { + this.contactInfo = newContactInfo; + touch(); + } + + public void updatePaymentTerms(PaymentTerms newPaymentTerms) { + this.paymentTerms = newPaymentTerms; + touch(); + } + + public void addCertificate(QualityCertificate certificate) { + if (!this.certificates.contains(certificate)) { + this.certificates.add(certificate); + touch(); + } + } + + public void removeCertificate(QualityCertificate certificate) { + this.certificates.remove(certificate); + touch(); + } + + public void rate(SupplierRating newRating) { + this.rating = newRating; + touch(); + } + + public void activate() { + this.status = SupplierStatus.ACTIVE; + touch(); + } + + public void deactivate() { + this.status = SupplierStatus.INACTIVE; + touch(); + } + + public List expiredCertificates() { + return certificates.stream() + .filter(QualityCertificate::isExpired) + .toList(); + } + + public List certificatesExpiringSoon(int days) { + return certificates.stream() + .filter(c -> c.expiresWithinDays(days)) + .toList(); + } + + // ==================== Helpers ==================== + + private void touch() { + this.updatedAt = LocalDateTime.now(); + } + + // ==================== Getters ==================== + + public SupplierId id() { return id; } + public SupplierName name() { return name; } + public Address address() { return address; } + public ContactInfo contactInfo() { return contactInfo; } + public PaymentTerms paymentTerms() { return paymentTerms; } + public List certificates() { return Collections.unmodifiableList(certificates); } + public SupplierRating rating() { return rating; } + public SupplierStatus status() { return status; } + public LocalDateTime createdAt() { return createdAt; } + public LocalDateTime updatedAt() { return updatedAt; } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Supplier other)) return false; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return "Supplier{id=" + id + ", name=" + name + ", status=" + status + "}"; + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SupplierError.java b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierError.java new file mode 100644 index 0000000..0a85548 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierError.java @@ -0,0 +1,30 @@ +package de.effigenix.domain.masterdata; + +public sealed interface SupplierError { + + String code(); + String message(); + + record SupplierNotFound(SupplierId id) implements SupplierError { + @Override public String code() { return "SUPPLIER_NOT_FOUND"; } + @Override public String message() { return "Supplier with ID '" + id.value() + "' not found"; } + } + + record SupplierNameAlreadyExists(String name) implements SupplierError { + @Override public String code() { return "SUPPLIER_NAME_EXISTS"; } + @Override public String message() { return "Supplier name '" + name + "' already exists"; } + } + + record InvalidRating(String reason) implements SupplierError { + @Override public String code() { return "INVALID_RATING"; } + @Override public String message() { return "Invalid rating: " + reason; } + } + + record Unauthorized(String message) implements SupplierError { + @Override public String code() { return "UNAUTHORIZED"; } + } + + record RepositoryFailure(String message) implements SupplierError { + @Override public String code() { return "REPOSITORY_ERROR"; } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SupplierId.java b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierId.java new file mode 100644 index 0000000..23dddf2 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierId.java @@ -0,0 +1,20 @@ +package de.effigenix.domain.masterdata; + +import java.util.UUID; + +public record SupplierId(String value) { + + public SupplierId { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("SupplierId must not be blank"); + } + } + + public static SupplierId generate() { + return new SupplierId(UUID.randomUUID().toString()); + } + + public static SupplierId of(String value) { + return new SupplierId(value); + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SupplierName.java b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierName.java new file mode 100644 index 0000000..8b67a6d --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierName.java @@ -0,0 +1,13 @@ +package de.effigenix.domain.masterdata; + +public record SupplierName(String value) { + + public SupplierName { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("SupplierName must not be blank"); + } + if (value.length() > 200) { + throw new IllegalArgumentException("SupplierName must not exceed 200 characters"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SupplierRating.java b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierRating.java new file mode 100644 index 0000000..0d6d8d0 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierRating.java @@ -0,0 +1,23 @@ +package de.effigenix.domain.masterdata; + +/** + * Value Object representing a supplier rating with three scores (1-5). + */ +public record SupplierRating(int qualityScore, int deliveryScore, int priceScore) { + + public SupplierRating { + validateScore("qualityScore", qualityScore); + validateScore("deliveryScore", deliveryScore); + validateScore("priceScore", priceScore); + } + + public double averageScore() { + return (qualityScore + deliveryScore + priceScore) / 3.0; + } + + private static void validateScore(String name, int score) { + if (score < 1 || score > 5) { + throw new IllegalArgumentException(name + " must be between 1 and 5, was: " + score); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SupplierRepository.java b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierRepository.java new file mode 100644 index 0000000..8c6f658 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierRepository.java @@ -0,0 +1,22 @@ +package de.effigenix.domain.masterdata; + +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; + +import java.util.List; +import java.util.Optional; + +public interface SupplierRepository { + + Result> findById(SupplierId id); + + Result> findAll(); + + Result> findByStatus(SupplierStatus status); + + Result save(Supplier supplier); + + Result delete(Supplier supplier); + + Result existsByName(SupplierName name); +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/SupplierStatus.java b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierStatus.java new file mode 100644 index 0000000..fe67c22 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/SupplierStatus.java @@ -0,0 +1,6 @@ +package de.effigenix.domain.masterdata; + +public enum SupplierStatus { + ACTIVE, + INACTIVE +} diff --git a/backend/src/main/java/de/effigenix/domain/masterdata/Unit.java b/backend/src/main/java/de/effigenix/domain/masterdata/Unit.java new file mode 100644 index 0000000..583e590 --- /dev/null +++ b/backend/src/main/java/de/effigenix/domain/masterdata/Unit.java @@ -0,0 +1,8 @@ +package de.effigenix.domain.masterdata; + +public enum Unit { + PIECE_FIXED, + KG, + HUNDRED_GRAM, + PIECE_VARIABLE +} diff --git a/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java b/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java index d618fba..6db3552 100644 --- a/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java +++ b/backend/src/main/java/de/effigenix/domain/usermanagement/RoleRepository.java @@ -1,5 +1,6 @@ package de.effigenix.domain.usermanagement; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import java.util.List; diff --git a/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java b/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java index 3c8bac1..37092c1 100644 --- a/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java +++ b/backend/src/main/java/de/effigenix/domain/usermanagement/UserRepository.java @@ -1,5 +1,6 @@ package de.effigenix.domain.usermanagement; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.Result; import java.util.List; diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java index 2a2fd0e..540f775 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaRoleRepository.java @@ -1,6 +1,6 @@ package de.effigenix.infrastructure.usermanagement.persistence.repository; -import de.effigenix.domain.usermanagement.RepositoryError; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.domain.usermanagement.Role; import de.effigenix.domain.usermanagement.RoleId; import de.effigenix.domain.usermanagement.RoleName; diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java index 2db1a10..e038dda 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/persistence/repository/JpaUserRepository.java @@ -1,6 +1,6 @@ package de.effigenix.infrastructure.usermanagement.persistence.repository; -import de.effigenix.domain.usermanagement.RepositoryError; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.domain.usermanagement.User; import de.effigenix.domain.usermanagement.UserId; import de.effigenix.domain.usermanagement.UserRepository; diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java index d25d697..8cf7ea5 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/controller/RoleController.java @@ -1,7 +1,7 @@ package de.effigenix.infrastructure.usermanagement.web.controller; import de.effigenix.application.usermanagement.dto.RoleDTO; -import de.effigenix.domain.usermanagement.RepositoryError; +import de.effigenix.shared.common.RepositoryError; import de.effigenix.domain.usermanagement.Role; import de.effigenix.domain.usermanagement.RoleRepository; import de.effigenix.shared.common.Result; diff --git a/backend/src/main/java/de/effigenix/shared/common/Address.java b/backend/src/main/java/de/effigenix/shared/common/Address.java new file mode 100644 index 0000000..7e43cd9 --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/Address.java @@ -0,0 +1,33 @@ +package de.effigenix.shared.common; + +/** + * Value Object representing a postal address. + * Immutable and self-validating. + */ +public record Address( + String street, + String houseNumber, + String postalCode, + String city, + String country +) { + + public Address { + if (street == null || street.isBlank()) { + throw new IllegalArgumentException("Street must not be blank"); + } + if (postalCode == null || postalCode.isBlank()) { + throw new IllegalArgumentException("Postal code must not be blank"); + } + if (city == null || city.isBlank()) { + throw new IllegalArgumentException("City must not be blank"); + } + if (country == null || country.isBlank()) { + throw new IllegalArgumentException("Country must not be blank"); + } + if (!country.matches("^[A-Z]{2}$")) { + throw new IllegalArgumentException("Country must be ISO 3166-1 alpha-2 code (e.g. 'DE')"); + } + // houseNumber is optional (nullable) + } +} diff --git a/backend/src/main/java/de/effigenix/shared/common/ContactInfo.java b/backend/src/main/java/de/effigenix/shared/common/ContactInfo.java new file mode 100644 index 0000000..674d79a --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/ContactInfo.java @@ -0,0 +1,23 @@ +package de.effigenix.shared.common; + +/** + * Value Object representing contact information. + * All fields are optional, but email format is validated when present. + * Immutable. + */ +public record ContactInfo( + String phone, + String email, + String contactPerson +) { + + public ContactInfo { + if (email != null && !email.isBlank() && !isValidEmail(email)) { + throw new IllegalArgumentException("Invalid email format: " + email); + } + } + + private static boolean isValidEmail(String email) { + return email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"); + } +} diff --git a/backend/src/main/java/de/effigenix/shared/common/Money.java b/backend/src/main/java/de/effigenix/shared/common/Money.java new file mode 100644 index 0000000..8a337e6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/Money.java @@ -0,0 +1,46 @@ +package de.effigenix.shared.common; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Currency; +import java.util.Objects; + +/** + * Value Object representing a monetary amount with currency. + * Immutable and self-validating. + */ +public record Money(BigDecimal amount, Currency currency) { + + public Money { + Objects.requireNonNull(amount, "Amount must not be null"); + Objects.requireNonNull(currency, "Currency must not be null"); + if (amount.scale() > 2) { + throw new IllegalArgumentException("Amount scale must not exceed 2 decimal places"); + } + if (amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Amount must not be negative"); + } + } + + public static Money euro(BigDecimal amount) { + return new Money(amount.setScale(2, RoundingMode.HALF_UP), Currency.getInstance("EUR")); + } + + public static Money zero() { + return new Money(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP), Currency.getInstance("EUR")); + } + + public Money add(Money other) { + if (!this.currency.equals(other.currency)) { + throw new IllegalArgumentException("Cannot add Money with different currencies"); + } + return new Money(this.amount.add(other.amount), this.currency); + } + + public Money multiply(BigDecimal factor) { + return new Money( + this.amount.multiply(factor).setScale(2, RoundingMode.HALF_UP), + this.currency + ); + } +} diff --git a/backend/src/main/java/de/effigenix/shared/common/PaymentTerms.java b/backend/src/main/java/de/effigenix/shared/common/PaymentTerms.java new file mode 100644 index 0000000..371f33d --- /dev/null +++ b/backend/src/main/java/de/effigenix/shared/common/PaymentTerms.java @@ -0,0 +1,14 @@ +package de.effigenix.shared.common; + +/** + * Value Object representing payment terms. + * Immutable and self-validating. + */ +public record PaymentTerms(int paymentDueDays, String description) { + + public PaymentTerms { + if (paymentDueDays < 0) { + throw new IllegalArgumentException("Payment due days must not be negative"); + } + } +} diff --git a/backend/src/main/java/de/effigenix/domain/usermanagement/RepositoryError.java b/backend/src/main/java/de/effigenix/shared/common/RepositoryError.java similarity index 83% rename from backend/src/main/java/de/effigenix/domain/usermanagement/RepositoryError.java rename to backend/src/main/java/de/effigenix/shared/common/RepositoryError.java index 83c35cf..b186ba5 100644 --- a/backend/src/main/java/de/effigenix/domain/usermanagement/RepositoryError.java +++ b/backend/src/main/java/de/effigenix/shared/common/RepositoryError.java @@ -1,7 +1,8 @@ -package de.effigenix.domain.usermanagement; +package de.effigenix.shared.common; /** * Repository operation errors. + * Shared Kernel — used by all Bounded Contexts. * Sealed interface ensures exhaustive handling. */ public sealed interface RepositoryError {