1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:19:35 +01:00

feat(masterdata): Infra-Layer für Supplier Aggregate

Liquibase-Migration (006), JPA-Entities (SupplierEntity,
QualityCertificateEmbeddable), Mapper, Spring-Data-Repo,
Domain-Repo-Adapter, Request-DTOs, SupplierController
(9 Endpoints), ErrorMapper und UseCaseConfiguration erweitert.
This commit is contained in:
Sebastian Frick 2026-02-18 13:22:46 +01:00
parent 8b2fd38192
commit 6ec07e7b34
16 changed files with 869 additions and 0 deletions

View file

@ -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);
}
}

View file

@ -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; }
}

View file

@ -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<QualityCertificateEmbeddable> 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<QualityCertificateEmbeddable> 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<QualityCertificateEmbeddable> certificates) { this.certificates = certificates; }
}

View file

@ -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<QualityCertificateEmbeddable> 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<QualityCertificate> 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()
);
}
}

View file

@ -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<RepositoryError, Optional<Supplier>> findById(SupplierId id) {
try {
Optional<Supplier> 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<RepositoryError, List<Supplier>> findAll() {
try {
List<Supplier> 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<RepositoryError, List<Supplier>> findByStatus(SupplierStatus status) {
try {
List<Supplier> 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<RepositoryError, Void> 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<RepositoryError, Void> 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<RepositoryError, Boolean> existsByName(SupplierName name) {
try {
return Result.success(jpaRepository.existsByName(name.value()));
} catch (Exception e) {
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
}

View file

@ -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<SupplierEntity, String> {
List<SupplierEntity> findByStatus(String status);
boolean existsByName(String name);
}

View file

@ -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<Supplier> 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<List<Supplier>> listSuppliers(
@RequestParam(value = "status", required = false) SupplierStatus status,
Authentication authentication
) {
var actorId = extractActorId(authentication);
logger.info("Listing suppliers by actor: {}", actorId.value());
Result<SupplierError, List<Supplier>> 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<Supplier> 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<Supplier> 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<Supplier> 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<Supplier> 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<Supplier> 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<Supplier> 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<Void> 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;
}
}
}

View file

@ -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
) {}

View file

@ -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
) {}

View file

@ -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
) {}

View file

@ -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
) {}

View file

@ -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
) {}

View file

@ -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;

View file

@ -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<ErrorResponse> 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.
*

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="006-create-suppliers-table" author="effigenix">
<createTable tableName="suppliers">
<column name="id" type="VARCHAR(36)">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="VARCHAR(200)">
<constraints nullable="false" unique="true"/>
</column>
<column name="phone" type="VARCHAR(50)"/>
<column name="email" type="VARCHAR(255)"/>
<column name="contact_person" type="VARCHAR(200)"/>
<column name="street" type="VARCHAR(200)"/>
<column name="house_number" type="VARCHAR(20)"/>
<column name="postal_code" type="VARCHAR(20)"/>
<column name="city" type="VARCHAR(100)"/>
<column name="country" type="VARCHAR(2)"/>
<column name="payment_due_days" type="INT"/>
<column name="payment_description" type="VARCHAR(500)"/>
<column name="quality_score" type="INT"/>
<column name="delivery_score" type="INT"/>
<column name="price_score" type="INT"/>
<column name="status" type="VARCHAR(20)" defaultValue="ACTIVE">
<constraints nullable="false"/>
</column>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP">
<constraints nullable="false"/>
</column>
</createTable>
<sql>
ALTER TABLE suppliers ADD CONSTRAINT chk_supplier_status CHECK (status IN ('ACTIVE', 'INACTIVE'));
</sql>
<createIndex tableName="suppliers" indexName="idx_suppliers_name">
<column name="name"/>
</createIndex>
<createIndex tableName="suppliers" indexName="idx_suppliers_status">
<column name="status"/>
</createIndex>
</changeSet>
<changeSet id="006-create-quality-certificates-table" author="effigenix">
<createTable tableName="quality_certificates">
<column name="supplier_id" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="certificate_type" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="issuer" type="VARCHAR(200)"/>
<column name="valid_from" type="DATE"/>
<column name="valid_until" type="DATE"/>
</createTable>
<addPrimaryKey tableName="quality_certificates"
columnNames="supplier_id, certificate_type, valid_from"
constraintName="pk_quality_certificates"/>
<addForeignKeyConstraint baseTableName="quality_certificates" baseColumnNames="supplier_id"
referencedTableName="suppliers" referencedColumnNames="id"
constraintName="fk_quality_certificates_supplier" onDelete="CASCADE"/>
<createIndex tableName="quality_certificates" indexName="idx_quality_certificates_supplier_id">
<column name="supplier_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View file

@ -10,5 +10,6 @@
<include file="db/changelog/changes/003-create-audit-logs-table.xml"/>
<include file="db/changelog/changes/004-seed-admin-user.xml"/>
<include file="db/changelog/changes/005-create-masterdata-schema.xml"/>
<include file="db/changelog/changes/006-create-supplier-schema.xml"/>
</databaseChangeLog>