From 6ec07e7b34492592acc276c0c94d5f3b07ca4f86 Mon Sep 17 00:00:00 2001 From: Sebastian Frick Date: Wed, 18 Feb 2026 13:22:46 +0100 Subject: [PATCH] =?UTF-8?q?feat(masterdata):=20Infra-Layer=20f=C3=BCr=20Su?= =?UTF-8?q?pplier=20Aggregate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Liquibase-Migration (006), JPA-Entities (SupplierEntity, QualityCertificateEmbeddable), Mapper, Spring-Data-Repo, Domain-Repo-Adapter, Request-DTOs, SupplierController (9 Endpoints), ErrorMapper und UseCaseConfiguration erweitert. --- .../MasterDataUseCaseConfiguration.java | 48 +++ .../entity/QualityCertificateEmbeddable.java | 41 +++ .../persistence/entity/SupplierEntity.java | 113 +++++++ .../persistence/mapper/SupplierMapper.java | 107 +++++++ .../repository/JpaSupplierRepository.java | 91 ++++++ .../repository/SupplierJpaRepository.java | 13 + .../web/controller/SupplierController.java | 283 ++++++++++++++++++ .../web/dto/AddCertificateRequest.java | 12 + .../web/dto/CreateSupplierRequest.java | 17 ++ .../web/dto/RateSupplierRequest.java | 10 + .../web/dto/RemoveCertificateRequest.java | 11 + .../web/dto/UpdateSupplierRequest.java | 15 + .../MasterDataErrorHttpStatusMapper.java | 13 + .../web/exception/GlobalExceptionHandler.java | 21 ++ .../changes/006-create-supplier-schema.xml | 73 +++++ .../db/changelog/db.changelog-master.xml | 1 + 16 files changed, 869 insertions(+) create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/QualityCertificateEmbeddable.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/SupplierEntity.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/SupplierMapper.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/SupplierJpaRepository.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddCertificateRequest.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateSupplierRequest.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RateSupplierRequest.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RemoveCertificateRequest.java create mode 100644 backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateSupplierRequest.java create mode 100644 backend/src/main/resources/db/changelog/changes/006-create-supplier-schema.xml diff --git a/backend/src/main/java/de/effigenix/infrastructure/config/MasterDataUseCaseConfiguration.java b/backend/src/main/java/de/effigenix/infrastructure/config/MasterDataUseCaseConfiguration.java index 5e67d91..d77b0df 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/config/MasterDataUseCaseConfiguration.java +++ b/backend/src/main/java/de/effigenix/infrastructure/config/MasterDataUseCaseConfiguration.java @@ -3,6 +3,7 @@ package de.effigenix.infrastructure.config; import de.effigenix.application.masterdata.*; import de.effigenix.domain.masterdata.ArticleRepository; import de.effigenix.domain.masterdata.ProductCategoryRepository; +import de.effigenix.domain.masterdata.SupplierRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -90,4 +91,51 @@ public class MasterDataUseCaseConfiguration { ) { return new DeleteProductCategory(categoryRepository, articleRepository); } + + // ==================== Supplier Use Cases ==================== + + @Bean + public CreateSupplier createSupplier(SupplierRepository supplierRepository) { + return new CreateSupplier(supplierRepository); + } + + @Bean + public UpdateSupplier updateSupplier(SupplierRepository supplierRepository) { + return new UpdateSupplier(supplierRepository); + } + + @Bean + public GetSupplier getSupplier(SupplierRepository supplierRepository) { + return new GetSupplier(supplierRepository); + } + + @Bean + public ListSuppliers listSuppliers(SupplierRepository supplierRepository) { + return new ListSuppliers(supplierRepository); + } + + @Bean + public ActivateSupplier activateSupplier(SupplierRepository supplierRepository) { + return new ActivateSupplier(supplierRepository); + } + + @Bean + public DeactivateSupplier deactivateSupplier(SupplierRepository supplierRepository) { + return new DeactivateSupplier(supplierRepository); + } + + @Bean + public RateSupplier rateSupplier(SupplierRepository supplierRepository) { + return new RateSupplier(supplierRepository); + } + + @Bean + public AddCertificate addCertificate(SupplierRepository supplierRepository) { + return new AddCertificate(supplierRepository); + } + + @Bean + public RemoveCertificate removeCertificate(SupplierRepository supplierRepository) { + return new RemoveCertificate(supplierRepository); + } } diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/QualityCertificateEmbeddable.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/QualityCertificateEmbeddable.java new file mode 100644 index 0000000..a6f48a8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/QualityCertificateEmbeddable.java @@ -0,0 +1,41 @@ +package de.effigenix.infrastructure.masterdata.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; + +import java.time.LocalDate; + +@Embeddable +public class QualityCertificateEmbeddable { + + @Column(name = "certificate_type", nullable = false, length = 100) + private String certificateType; + + @Column(name = "issuer", length = 200) + private String issuer; + + @Column(name = "valid_from") + private LocalDate validFrom; + + @Column(name = "valid_until") + private LocalDate validUntil; + + protected QualityCertificateEmbeddable() {} + + public QualityCertificateEmbeddable(String certificateType, String issuer, LocalDate validFrom, LocalDate validUntil) { + this.certificateType = certificateType; + this.issuer = issuer; + this.validFrom = validFrom; + this.validUntil = validUntil; + } + + public String getCertificateType() { return certificateType; } + public String getIssuer() { return issuer; } + public LocalDate getValidFrom() { return validFrom; } + public LocalDate getValidUntil() { return validUntil; } + + public void setCertificateType(String certificateType) { this.certificateType = certificateType; } + public void setIssuer(String issuer) { this.issuer = issuer; } + public void setValidFrom(LocalDate validFrom) { this.validFrom = validFrom; } + public void setValidUntil(LocalDate validUntil) { this.validUntil = validUntil; } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/SupplierEntity.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/SupplierEntity.java new file mode 100644 index 0000000..918bb88 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/entity/SupplierEntity.java @@ -0,0 +1,113 @@ +package de.effigenix.infrastructure.masterdata.persistence.entity; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "suppliers") +public class SupplierEntity { + + @Id + @Column(name = "id", nullable = false, length = 36) + private String id; + + @Column(name = "name", nullable = false, unique = true, length = 200) + private String name; + + @Column(name = "phone", length = 50) + private String phone; + + @Column(name = "email", length = 255) + private String email; + + @Column(name = "contact_person", length = 200) + private String contactPerson; + + @Column(name = "street", length = 200) + private String street; + + @Column(name = "house_number", length = 20) + private String houseNumber; + + @Column(name = "postal_code", length = 20) + private String postalCode; + + @Column(name = "city", length = 100) + private String city; + + @Column(name = "country", length = 2) + private String country; + + @Column(name = "payment_due_days") + private Integer paymentDueDays; + + @Column(name = "payment_description", length = 500) + private String paymentDescription; + + @Column(name = "quality_score") + private Integer qualityScore; + + @Column(name = "delivery_score") + private Integer deliveryScore; + + @Column(name = "price_score") + private Integer priceScore; + + @Column(name = "status", nullable = false, length = 20) + private String status; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "quality_certificates", joinColumns = @JoinColumn(name = "supplier_id")) + private List certificates = new ArrayList<>(); + + public SupplierEntity() {} + + public String getId() { return id; } + public String getName() { return name; } + public String getPhone() { return phone; } + public String getEmail() { return email; } + public String getContactPerson() { return contactPerson; } + public String getStreet() { return street; } + public String getHouseNumber() { return houseNumber; } + public String getPostalCode() { return postalCode; } + public String getCity() { return city; } + public String getCountry() { return country; } + public Integer getPaymentDueDays() { return paymentDueDays; } + public String getPaymentDescription() { return paymentDescription; } + public Integer getQualityScore() { return qualityScore; } + public Integer getDeliveryScore() { return deliveryScore; } + public Integer getPriceScore() { return priceScore; } + public String getStatus() { return status; } + public LocalDateTime getCreatedAt() { return createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public List getCertificates() { return certificates; } + + public void setId(String id) { this.id = id; } + public void setName(String name) { this.name = name; } + public void setPhone(String phone) { this.phone = phone; } + public void setEmail(String email) { this.email = email; } + public void setContactPerson(String contactPerson) { this.contactPerson = contactPerson; } + public void setStreet(String street) { this.street = street; } + public void setHouseNumber(String houseNumber) { this.houseNumber = houseNumber; } + public void setPostalCode(String postalCode) { this.postalCode = postalCode; } + public void setCity(String city) { this.city = city; } + public void setCountry(String country) { this.country = country; } + public void setPaymentDueDays(Integer paymentDueDays) { this.paymentDueDays = paymentDueDays; } + public void setPaymentDescription(String paymentDescription) { this.paymentDescription = paymentDescription; } + public void setQualityScore(Integer qualityScore) { this.qualityScore = qualityScore; } + public void setDeliveryScore(Integer deliveryScore) { this.deliveryScore = deliveryScore; } + public void setPriceScore(Integer priceScore) { this.priceScore = priceScore; } + public void setStatus(String status) { this.status = status; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + public void setCertificates(List certificates) { this.certificates = certificates; } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/SupplierMapper.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/SupplierMapper.java new file mode 100644 index 0000000..5d0e3f8 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/mapper/SupplierMapper.java @@ -0,0 +1,107 @@ +package de.effigenix.infrastructure.masterdata.persistence.mapper; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.infrastructure.masterdata.persistence.entity.QualityCertificateEmbeddable; +import de.effigenix.infrastructure.masterdata.persistence.entity.SupplierEntity; +import de.effigenix.shared.common.Address; +import de.effigenix.shared.common.ContactInfo; +import de.effigenix.shared.common.PaymentTerms; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class SupplierMapper { + + public SupplierEntity toEntity(Supplier supplier) { + var entity = new SupplierEntity(); + entity.setId(supplier.id().value()); + entity.setName(supplier.name().value()); + + var contact = supplier.contactInfo(); + if (contact != null) { + entity.setPhone(contact.phone()); + entity.setEmail(contact.email()); + entity.setContactPerson(contact.contactPerson()); + } + + var address = supplier.address(); + if (address != null) { + entity.setStreet(address.street()); + entity.setHouseNumber(address.houseNumber()); + entity.setPostalCode(address.postalCode()); + entity.setCity(address.city()); + entity.setCountry(address.country()); + } + + var terms = supplier.paymentTerms(); + if (terms != null) { + entity.setPaymentDueDays(terms.paymentDueDays()); + entity.setPaymentDescription(terms.description()); + } + + var rating = supplier.rating(); + if (rating != null) { + entity.setQualityScore(rating.qualityScore()); + entity.setDeliveryScore(rating.deliveryScore()); + entity.setPriceScore(rating.priceScore()); + } + + entity.setStatus(supplier.status().name()); + entity.setCreatedAt(supplier.createdAt()); + entity.setUpdatedAt(supplier.updatedAt()); + + List certs = supplier.certificates().stream() + .map(c -> new QualityCertificateEmbeddable( + c.certificateType(), c.issuer(), c.validFrom(), c.validUntil())) + .collect(Collectors.toList()); + entity.setCertificates(certs); + + return entity; + } + + public Supplier toDomain(SupplierEntity entity) { + Address address = null; + if (entity.getStreet() != null) { + address = new Address( + entity.getStreet(), entity.getHouseNumber(), + entity.getPostalCode(), entity.getCity(), entity.getCountry() + ); + } + + ContactInfo contactInfo = new ContactInfo( + entity.getPhone(), entity.getEmail(), entity.getContactPerson() + ); + + PaymentTerms paymentTerms = null; + if (entity.getPaymentDueDays() != null) { + paymentTerms = new PaymentTerms(entity.getPaymentDueDays(), entity.getPaymentDescription()); + } + + SupplierRating rating = null; + if (entity.getQualityScore() != null) { + rating = new SupplierRating( + entity.getQualityScore(), entity.getDeliveryScore(), entity.getPriceScore() + ); + } + + List certificates = entity.getCertificates().stream() + .map(c -> new QualityCertificate( + c.getCertificateType(), c.getIssuer(), c.getValidFrom(), c.getValidUntil())) + .collect(Collectors.toList()); + + return Supplier.reconstitute( + SupplierId.of(entity.getId()), + new SupplierName(entity.getName()), + address, + contactInfo, + paymentTerms, + certificates, + rating, + SupplierStatus.valueOf(entity.getStatus()), + entity.getCreatedAt(), + entity.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java new file mode 100644 index 0000000..4af2358 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/JpaSupplierRepository.java @@ -0,0 +1,91 @@ +package de.effigenix.infrastructure.masterdata.persistence.repository; + +import de.effigenix.domain.masterdata.*; +import de.effigenix.infrastructure.masterdata.persistence.mapper.SupplierMapper; +import de.effigenix.shared.common.RepositoryError; +import de.effigenix.shared.common.Result; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Repository +@Transactional(readOnly = true) +public class JpaSupplierRepository implements SupplierRepository { + + private final SupplierJpaRepository jpaRepository; + private final SupplierMapper mapper; + + public JpaSupplierRepository(SupplierJpaRepository jpaRepository, SupplierMapper mapper) { + this.jpaRepository = jpaRepository; + this.mapper = mapper; + } + + @Override + public Result> findById(SupplierId id) { + try { + Optional result = jpaRepository.findById(id.value()) + .map(mapper::toDomain); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findAll() { + try { + List result = jpaRepository.findAll().stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result> findByStatus(SupplierStatus status) { + try { + List result = jpaRepository.findByStatus(status.name()).stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + return Result.success(result); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + @Transactional + public Result save(Supplier supplier) { + try { + jpaRepository.save(mapper.toEntity(supplier)); + return Result.success(null); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + @Transactional + public Result delete(Supplier supplier) { + try { + jpaRepository.deleteById(supplier.id().value()); + return Result.success(null); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } + + @Override + public Result existsByName(SupplierName name) { + try { + return Result.success(jpaRepository.existsByName(name.value())); + } catch (Exception e) { + return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/SupplierJpaRepository.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/SupplierJpaRepository.java new file mode 100644 index 0000000..7af358b --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/persistence/repository/SupplierJpaRepository.java @@ -0,0 +1,13 @@ +package de.effigenix.infrastructure.masterdata.persistence.repository; + +import de.effigenix.infrastructure.masterdata.persistence.entity.SupplierEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SupplierJpaRepository extends JpaRepository { + + List findByStatus(String status); + + boolean existsByName(String name); +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java new file mode 100644 index 0000000..d0083d6 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/controller/SupplierController.java @@ -0,0 +1,283 @@ +package de.effigenix.infrastructure.masterdata.web.controller; + +import de.effigenix.application.masterdata.*; +import de.effigenix.application.masterdata.command.*; +import de.effigenix.domain.masterdata.Supplier; +import de.effigenix.domain.masterdata.SupplierError; +import de.effigenix.domain.masterdata.SupplierId; +import de.effigenix.domain.masterdata.SupplierStatus; +import de.effigenix.infrastructure.masterdata.web.dto.*; +import de.effigenix.shared.common.Result; +import de.effigenix.shared.security.ActorId; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/suppliers") +@SecurityRequirement(name = "Bearer Authentication") +@Tag(name = "Suppliers", description = "Supplier management endpoints") +public class SupplierController { + + private static final Logger logger = LoggerFactory.getLogger(SupplierController.class); + + private final CreateSupplier createSupplier; + private final UpdateSupplier updateSupplier; + private final GetSupplier getSupplier; + private final ListSuppliers listSuppliers; + private final ActivateSupplier activateSupplier; + private final DeactivateSupplier deactivateSupplier; + private final RateSupplier rateSupplier; + private final AddCertificate addCertificate; + private final RemoveCertificate removeCertificate; + + public SupplierController( + CreateSupplier createSupplier, + UpdateSupplier updateSupplier, + GetSupplier getSupplier, + ListSuppliers listSuppliers, + ActivateSupplier activateSupplier, + DeactivateSupplier deactivateSupplier, + RateSupplier rateSupplier, + AddCertificate addCertificate, + RemoveCertificate removeCertificate + ) { + this.createSupplier = createSupplier; + this.updateSupplier = updateSupplier; + this.getSupplier = getSupplier; + this.listSuppliers = listSuppliers; + this.activateSupplier = activateSupplier; + this.deactivateSupplier = deactivateSupplier; + this.rateSupplier = rateSupplier; + this.addCertificate = addCertificate; + this.removeCertificate = removeCertificate; + } + + @PostMapping + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity createSupplier( + @Valid @RequestBody CreateSupplierRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Creating supplier: {} by actor: {}", request.name(), actorId.value()); + + var cmd = new CreateSupplierCommand( + request.name(), request.phone(), request.email(), request.contactPerson(), + request.street(), request.houseNumber(), request.postalCode(), + request.city(), request.country(), + request.paymentDueDays(), request.paymentDescription() + ); + var result = createSupplier.execute(cmd, actorId); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + logger.info("Supplier created: {}", request.name()); + return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + } + + @GetMapping + public ResponseEntity> listSuppliers( + @RequestParam(value = "status", required = false) SupplierStatus status, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Listing suppliers by actor: {}", actorId.value()); + + Result> result; + if (status != null) { + result = listSuppliers.executeByStatus(status); + } else { + result = listSuppliers.execute(); + } + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @GetMapping("/{id}") + public ResponseEntity getSupplier( + @PathVariable("id") String supplierId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Getting supplier: {} by actor: {}", supplierId, actorId.value()); + + var result = getSupplier.execute(SupplierId.of(supplierId)); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity updateSupplier( + @PathVariable("id") String supplierId, + @Valid @RequestBody UpdateSupplierRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Updating supplier: {} by actor: {}", supplierId, actorId.value()); + + var cmd = new UpdateSupplierCommand( + supplierId, + request.name(), request.phone(), request.email(), request.contactPerson(), + request.street(), request.houseNumber(), request.postalCode(), + request.city(), request.country(), + request.paymentDueDays(), request.paymentDescription() + ); + var result = updateSupplier.execute(cmd, actorId); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + logger.info("Supplier updated: {}", supplierId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/activate") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity activate( + @PathVariable("id") String supplierId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Activating supplier: {} by actor: {}", supplierId, actorId.value()); + + var result = activateSupplier.execute(SupplierId.of(supplierId), actorId); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + logger.info("Supplier activated: {}", supplierId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/deactivate") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity deactivate( + @PathVariable("id") String supplierId, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Deactivating supplier: {} by actor: {}", supplierId, actorId.value()); + + var result = deactivateSupplier.execute(SupplierId.of(supplierId), actorId); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + logger.info("Supplier deactivated: {}", supplierId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/rating") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity rateSupplier( + @PathVariable("id") String supplierId, + @Valid @RequestBody RateSupplierRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Rating supplier: {} by actor: {}", supplierId, actorId.value()); + + var cmd = new RateSupplierCommand( + supplierId, request.qualityScore(), request.deliveryScore(), request.priceScore() + ); + var result = rateSupplier.execute(cmd, actorId); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + logger.info("Supplier rated: {}", supplierId); + return ResponseEntity.ok(result.unsafeGetValue()); + } + + @PostMapping("/{id}/certificates") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity addCertificate( + @PathVariable("id") String supplierId, + @Valid @RequestBody AddCertificateRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Adding certificate to supplier: {} by actor: {}", supplierId, actorId.value()); + + var cmd = new AddCertificateCommand( + supplierId, request.certificateType(), request.issuer(), + request.validFrom(), request.validUntil() + ); + var result = addCertificate.execute(cmd, actorId); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + logger.info("Certificate added to supplier: {}", supplierId); + return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue()); + } + + @DeleteMapping("/{id}/certificates") + @PreAuthorize("hasAuthority('MASTERDATA_WRITE')") + public ResponseEntity removeCertificate( + @PathVariable("id") String supplierId, + @Valid @RequestBody RemoveCertificateRequest request, + Authentication authentication + ) { + var actorId = extractActorId(authentication); + logger.info("Removing certificate from supplier: {} by actor: {}", supplierId, actorId.value()); + + var cmd = new RemoveCertificateCommand( + supplierId, request.certificateType(), request.issuer(), request.validFrom() + ); + var result = removeCertificate.execute(cmd, actorId); + + if (result.isFailure()) { + throw new SupplierDomainErrorException(result.unsafeGetError()); + } + + logger.info("Certificate removed from supplier: {}", supplierId); + return ResponseEntity.noContent().build(); + } + + private ActorId extractActorId(Authentication authentication) { + if (authentication == null || authentication.getName() == null) { + throw new IllegalStateException("No authentication found in SecurityContext"); + } + return ActorId.of(authentication.getName()); + } + + public static class SupplierDomainErrorException extends RuntimeException { + private final SupplierError error; + + public SupplierDomainErrorException(SupplierError error) { + super(error.message()); + this.error = error; + } + + public SupplierError getError() { + return error; + } + } +} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddCertificateRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddCertificateRequest.java new file mode 100644 index 0000000..5d1246b --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/AddCertificateRequest.java @@ -0,0 +1,12 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import jakarta.validation.constraints.NotBlank; + +import java.time.LocalDate; + +public record AddCertificateRequest( + @NotBlank String certificateType, + String issuer, + LocalDate validFrom, + LocalDate validUntil +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateSupplierRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateSupplierRequest.java new file mode 100644 index 0000000..3604bcd --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/CreateSupplierRequest.java @@ -0,0 +1,17 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import jakarta.validation.constraints.NotBlank; + +public record CreateSupplierRequest( + @NotBlank String name, + @NotBlank String phone, + String email, + String contactPerson, + String street, + String houseNumber, + String postalCode, + String city, + String country, + Integer paymentDueDays, + String paymentDescription +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RateSupplierRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RateSupplierRequest.java new file mode 100644 index 0000000..df634e3 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RateSupplierRequest.java @@ -0,0 +1,10 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; + +public record RateSupplierRequest( + @Min(1) @Max(5) int qualityScore, + @Min(1) @Max(5) int deliveryScore, + @Min(1) @Max(5) int priceScore +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RemoveCertificateRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RemoveCertificateRequest.java new file mode 100644 index 0000000..79030ec --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/RemoveCertificateRequest.java @@ -0,0 +1,11 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +import jakarta.validation.constraints.NotBlank; + +import java.time.LocalDate; + +public record RemoveCertificateRequest( + @NotBlank String certificateType, + String issuer, + LocalDate validFrom +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateSupplierRequest.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateSupplierRequest.java new file mode 100644 index 0000000..09ce665 --- /dev/null +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/dto/UpdateSupplierRequest.java @@ -0,0 +1,15 @@ +package de.effigenix.infrastructure.masterdata.web.dto; + +public record UpdateSupplierRequest( + String name, + String phone, + String email, + String contactPerson, + String street, + String houseNumber, + String postalCode, + String city, + String country, + Integer paymentDueDays, + String paymentDescription +) {} diff --git a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/exception/MasterDataErrorHttpStatusMapper.java b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/exception/MasterDataErrorHttpStatusMapper.java index 7089cee..1cae856 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/exception/MasterDataErrorHttpStatusMapper.java +++ b/backend/src/main/java/de/effigenix/infrastructure/masterdata/web/exception/MasterDataErrorHttpStatusMapper.java @@ -2,6 +2,7 @@ package de.effigenix.infrastructure.masterdata.web.exception; import de.effigenix.domain.masterdata.ArticleError; import de.effigenix.domain.masterdata.ProductCategoryError; +import de.effigenix.domain.masterdata.SupplierError; public final class MasterDataErrorHttpStatusMapper { @@ -22,6 +23,18 @@ public final class MasterDataErrorHttpStatusMapper { }; } + public static int toHttpStatus(SupplierError error) { + return switch (error) { + case SupplierError.SupplierNotFound e -> 404; + case SupplierError.CertificateNotFound e -> 404; + case SupplierError.SupplierNameAlreadyExists e -> 409; + case SupplierError.InvalidRating e -> 400; + case SupplierError.ValidationFailure e -> 400; + case SupplierError.Unauthorized e -> 403; + case SupplierError.RepositoryFailure e -> 500; + }; + } + public static int toHttpStatus(ProductCategoryError error) { return switch (error) { case ProductCategoryError.CategoryNotFound e -> 404; diff --git a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java index 134269e..2eae45c 100644 --- a/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/de/effigenix/infrastructure/usermanagement/web/exception/GlobalExceptionHandler.java @@ -2,9 +2,11 @@ package de.effigenix.infrastructure.usermanagement.web.exception; import de.effigenix.domain.masterdata.ArticleError; import de.effigenix.domain.masterdata.ProductCategoryError; +import de.effigenix.domain.masterdata.SupplierError; import de.effigenix.domain.usermanagement.UserError; import de.effigenix.infrastructure.masterdata.web.controller.ArticleController; import de.effigenix.infrastructure.masterdata.web.controller.ProductCategoryController; +import de.effigenix.infrastructure.masterdata.web.controller.SupplierController; import de.effigenix.infrastructure.masterdata.web.exception.MasterDataErrorHttpStatusMapper; import de.effigenix.infrastructure.usermanagement.web.controller.AuthController; import de.effigenix.infrastructure.usermanagement.web.controller.UserController; @@ -135,6 +137,25 @@ public class GlobalExceptionHandler { return ResponseEntity.status(status).body(errorResponse); } + @ExceptionHandler(SupplierController.SupplierDomainErrorException.class) + public ResponseEntity handleSupplierDomainError( + SupplierController.SupplierDomainErrorException ex, + HttpServletRequest request + ) { + SupplierError error = ex.getError(); + int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error); + logger.warn("Supplier domain error: {} - {}", error.code(), error.message()); + + ErrorResponse errorResponse = ErrorResponse.from( + error.code(), + error.message(), + status, + request.getRequestURI() + ); + + return ResponseEntity.status(status).body(errorResponse); + } + /** * Handles validation errors from @Valid annotations. * diff --git a/backend/src/main/resources/db/changelog/changes/006-create-supplier-schema.xml b/backend/src/main/resources/db/changelog/changes/006-create-supplier-schema.xml new file mode 100644 index 0000000..c33405b --- /dev/null +++ b/backend/src/main/resources/db/changelog/changes/006-create-supplier-schema.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ALTER TABLE suppliers ADD CONSTRAINT chk_supplier_status CHECK (status IN ('ACTIVE', 'INACTIVE')); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.xml b/backend/src/main/resources/db/changelog/db.changelog-master.xml index 0f4a908..1b369bc 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.xml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.xml @@ -10,5 +10,6 @@ +