mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:09:35 +01:00
feat(masterdata): Infra-Layer für Customer Aggregate
Liquibase-Migration (007), JPA-Entities (Customer, FrameContract, DeliveryAddress, ContractLineItem), Mapper, Repository-Adapter, 5 Request-DTOs, CustomerController (11 Endpoints), Error-Mapping und 11 Use-Case-Beans in MasterDataUseCaseConfiguration.
This commit is contained in:
parent
6ec07e7b34
commit
797f435a49
19 changed files with 1243 additions and 0 deletions
|
|
@ -2,6 +2,7 @@ package de.effigenix.infrastructure.config;
|
|||
|
||||
import de.effigenix.application.masterdata.*;
|
||||
import de.effigenix.domain.masterdata.ArticleRepository;
|
||||
import de.effigenix.domain.masterdata.CustomerRepository;
|
||||
import de.effigenix.domain.masterdata.ProductCategoryRepository;
|
||||
import de.effigenix.domain.masterdata.SupplierRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
|
@ -138,4 +139,61 @@ public class MasterDataUseCaseConfiguration {
|
|||
public RemoveCertificate removeCertificate(SupplierRepository supplierRepository) {
|
||||
return new RemoveCertificate(supplierRepository);
|
||||
}
|
||||
|
||||
// ==================== Customer Use Cases ====================
|
||||
|
||||
@Bean
|
||||
public CreateCustomer createCustomer(CustomerRepository customerRepository) {
|
||||
return new CreateCustomer(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UpdateCustomer updateCustomer(CustomerRepository customerRepository) {
|
||||
return new UpdateCustomer(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GetCustomer getCustomer(CustomerRepository customerRepository) {
|
||||
return new GetCustomer(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ListCustomers listCustomers(CustomerRepository customerRepository) {
|
||||
return new ListCustomers(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ActivateCustomer activateCustomer(CustomerRepository customerRepository) {
|
||||
return new ActivateCustomer(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DeactivateCustomer deactivateCustomer(CustomerRepository customerRepository) {
|
||||
return new DeactivateCustomer(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AddDeliveryAddress addDeliveryAddress(CustomerRepository customerRepository) {
|
||||
return new AddDeliveryAddress(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RemoveDeliveryAddress removeDeliveryAddress(CustomerRepository customerRepository) {
|
||||
return new RemoveDeliveryAddress(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SetFrameContract setFrameContract(CustomerRepository customerRepository) {
|
||||
return new SetFrameContract(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RemoveFrameContract removeFrameContract(CustomerRepository customerRepository) {
|
||||
return new RemoveFrameContract(customerRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SetPreferences setPreferences(CustomerRepository customerRepository) {
|
||||
return new SetPreferences(customerRepository);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Embeddable
|
||||
public class ContractLineItemEmbeddable {
|
||||
|
||||
@Column(name = "article_id", nullable = false, length = 36)
|
||||
private String articleId;
|
||||
|
||||
@Column(name = "agreed_price_amount", nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal agreedPriceAmount;
|
||||
|
||||
@Column(name = "agreed_price_currency", nullable = false, length = 3)
|
||||
private String agreedPriceCurrency;
|
||||
|
||||
@Column(name = "agreed_quantity", precision = 19, scale = 4)
|
||||
private BigDecimal agreedQuantity;
|
||||
|
||||
@Column(name = "unit", length = 30)
|
||||
private String unit;
|
||||
|
||||
protected ContractLineItemEmbeddable() {}
|
||||
|
||||
public ContractLineItemEmbeddable(String articleId, BigDecimal agreedPriceAmount,
|
||||
String agreedPriceCurrency, BigDecimal agreedQuantity, String unit) {
|
||||
this.articleId = articleId;
|
||||
this.agreedPriceAmount = agreedPriceAmount;
|
||||
this.agreedPriceCurrency = agreedPriceCurrency;
|
||||
this.agreedQuantity = agreedQuantity;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public String getArticleId() { return articleId; }
|
||||
public BigDecimal getAgreedPriceAmount() { return agreedPriceAmount; }
|
||||
public String getAgreedPriceCurrency() { return agreedPriceCurrency; }
|
||||
public BigDecimal getAgreedQuantity() { return agreedQuantity; }
|
||||
public String getUnit() { return unit; }
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "customers")
|
||||
public class CustomerEntity {
|
||||
|
||||
@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 = "type", nullable = false, length = 10)
|
||||
private String type;
|
||||
|
||||
@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 = "billing_street", nullable = false, length = 200)
|
||||
private String billingStreet;
|
||||
|
||||
@Column(name = "billing_house_number", length = 20)
|
||||
private String billingHouseNumber;
|
||||
|
||||
@Column(name = "billing_postal_code", nullable = false, length = 20)
|
||||
private String billingPostalCode;
|
||||
|
||||
@Column(name = "billing_city", nullable = false, length = 100)
|
||||
private String billingCity;
|
||||
|
||||
@Column(name = "billing_country", nullable = false, length = 2)
|
||||
private String billingCountry;
|
||||
|
||||
@Column(name = "payment_due_days")
|
||||
private Integer paymentDueDays;
|
||||
|
||||
@Column(name = "payment_description", length = 500)
|
||||
private String paymentDescription;
|
||||
|
||||
@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 = "delivery_addresses", joinColumns = @JoinColumn(name = "customer_id"))
|
||||
private List<DeliveryAddressEmbeddable> deliveryAddresses = new ArrayList<>();
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "customer_preferences", joinColumns = @JoinColumn(name = "customer_id"))
|
||||
@Column(name = "preference")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Set<de.effigenix.domain.masterdata.CustomerPreference> preferences = new HashSet<>();
|
||||
|
||||
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "id", referencedColumnName = "customer_id", insertable = false, updatable = false)
|
||||
private FrameContractEntity frameContract;
|
||||
|
||||
public CustomerEntity() {}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public String getType() { return type; }
|
||||
public String getPhone() { return phone; }
|
||||
public String getEmail() { return email; }
|
||||
public String getContactPerson() { return contactPerson; }
|
||||
public String getBillingStreet() { return billingStreet; }
|
||||
public String getBillingHouseNumber() { return billingHouseNumber; }
|
||||
public String getBillingPostalCode() { return billingPostalCode; }
|
||||
public String getBillingCity() { return billingCity; }
|
||||
public String getBillingCountry() { return billingCountry; }
|
||||
public Integer getPaymentDueDays() { return paymentDueDays; }
|
||||
public String getPaymentDescription() { return paymentDescription; }
|
||||
public String getStatus() { return status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public List<DeliveryAddressEmbeddable> getDeliveryAddresses() { return deliveryAddresses; }
|
||||
public Set<de.effigenix.domain.masterdata.CustomerPreference> getPreferences() { return preferences; }
|
||||
public FrameContractEntity getFrameContract() { return frameContract; }
|
||||
|
||||
public void setId(String id) { this.id = id; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
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 setBillingStreet(String billingStreet) { this.billingStreet = billingStreet; }
|
||||
public void setBillingHouseNumber(String billingHouseNumber) { this.billingHouseNumber = billingHouseNumber; }
|
||||
public void setBillingPostalCode(String billingPostalCode) { this.billingPostalCode = billingPostalCode; }
|
||||
public void setBillingCity(String billingCity) { this.billingCity = billingCity; }
|
||||
public void setBillingCountry(String billingCountry) { this.billingCountry = billingCountry; }
|
||||
public void setPaymentDueDays(Integer paymentDueDays) { this.paymentDueDays = paymentDueDays; }
|
||||
public void setPaymentDescription(String paymentDescription) { this.paymentDescription = paymentDescription; }
|
||||
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 setDeliveryAddresses(List<DeliveryAddressEmbeddable> deliveryAddresses) { this.deliveryAddresses = deliveryAddresses; }
|
||||
public void setPreferences(Set<de.effigenix.domain.masterdata.CustomerPreference> preferences) { this.preferences = preferences; }
|
||||
public void setFrameContract(FrameContractEntity frameContract) { this.frameContract = frameContract; }
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
|
||||
@Embeddable
|
||||
public class DeliveryAddressEmbeddable {
|
||||
|
||||
@Column(name = "label", length = 100)
|
||||
private String label;
|
||||
|
||||
@Column(name = "street", nullable = false, length = 200)
|
||||
private String street;
|
||||
|
||||
@Column(name = "house_number", length = 20)
|
||||
private String houseNumber;
|
||||
|
||||
@Column(name = "postal_code", nullable = false, length = 20)
|
||||
private String postalCode;
|
||||
|
||||
@Column(name = "city", nullable = false, length = 100)
|
||||
private String city;
|
||||
|
||||
@Column(name = "country", nullable = false, length = 2)
|
||||
private String country;
|
||||
|
||||
@Column(name = "contact_person", length = 200)
|
||||
private String contactPerson;
|
||||
|
||||
@Column(name = "delivery_notes", length = 500)
|
||||
private String deliveryNotes;
|
||||
|
||||
protected DeliveryAddressEmbeddable() {}
|
||||
|
||||
public DeliveryAddressEmbeddable(String label, String street, String houseNumber,
|
||||
String postalCode, String city, String country,
|
||||
String contactPerson, String deliveryNotes) {
|
||||
this.label = label;
|
||||
this.street = street;
|
||||
this.houseNumber = houseNumber;
|
||||
this.postalCode = postalCode;
|
||||
this.city = city;
|
||||
this.country = country;
|
||||
this.contactPerson = contactPerson;
|
||||
this.deliveryNotes = deliveryNotes;
|
||||
}
|
||||
|
||||
public String getLabel() { return label; }
|
||||
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 String getContactPerson() { return contactPerson; }
|
||||
public String getDeliveryNotes() { return deliveryNotes; }
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "frame_contracts")
|
||||
public class FrameContractEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "id", nullable = false, length = 36)
|
||||
private String id;
|
||||
|
||||
@Column(name = "customer_id", nullable = false, unique = true, length = 36)
|
||||
private String customerId;
|
||||
|
||||
@Column(name = "valid_from")
|
||||
private LocalDate validFrom;
|
||||
|
||||
@Column(name = "valid_until")
|
||||
private LocalDate validUntil;
|
||||
|
||||
@Column(name = "delivery_rhythm", nullable = false, length = 20)
|
||||
private String deliveryRhythm;
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "contract_line_items", joinColumns = @JoinColumn(name = "frame_contract_id"))
|
||||
private List<ContractLineItemEmbeddable> lineItems = new ArrayList<>();
|
||||
|
||||
protected FrameContractEntity() {}
|
||||
|
||||
public FrameContractEntity(String id, String customerId, LocalDate validFrom,
|
||||
LocalDate validUntil, String deliveryRhythm) {
|
||||
this.id = id;
|
||||
this.customerId = customerId;
|
||||
this.validFrom = validFrom;
|
||||
this.validUntil = validUntil;
|
||||
this.deliveryRhythm = deliveryRhythm;
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getCustomerId() { return customerId; }
|
||||
public LocalDate getValidFrom() { return validFrom; }
|
||||
public LocalDate getValidUntil() { return validUntil; }
|
||||
public String getDeliveryRhythm() { return deliveryRhythm; }
|
||||
public List<ContractLineItemEmbeddable> getLineItems() { return lineItems; }
|
||||
|
||||
public void setId(String id) { this.id = id; }
|
||||
public void setCustomerId(String customerId) { this.customerId = customerId; }
|
||||
public void setValidFrom(LocalDate validFrom) { this.validFrom = validFrom; }
|
||||
public void setValidUntil(LocalDate validUntil) { this.validUntil = validUntil; }
|
||||
public void setDeliveryRhythm(String deliveryRhythm) { this.deliveryRhythm = deliveryRhythm; }
|
||||
public void setLineItems(List<ContractLineItemEmbeddable> lineItems) { this.lineItems = lineItems; }
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.mapper;
|
||||
|
||||
import de.effigenix.domain.masterdata.*;
|
||||
import de.effigenix.infrastructure.masterdata.persistence.entity.*;
|
||||
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 org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class CustomerMapper {
|
||||
|
||||
public CustomerEntity toEntity(Customer customer) {
|
||||
var entity = new CustomerEntity();
|
||||
entity.setId(customer.id().value());
|
||||
entity.setName(customer.name().value());
|
||||
entity.setType(customer.type().name());
|
||||
|
||||
var contact = customer.contactInfo();
|
||||
if (contact != null) {
|
||||
entity.setPhone(contact.phone());
|
||||
entity.setEmail(contact.email());
|
||||
entity.setContactPerson(contact.contactPerson());
|
||||
}
|
||||
|
||||
var billing = customer.billingAddress();
|
||||
entity.setBillingStreet(billing.street());
|
||||
entity.setBillingHouseNumber(billing.houseNumber());
|
||||
entity.setBillingPostalCode(billing.postalCode());
|
||||
entity.setBillingCity(billing.city());
|
||||
entity.setBillingCountry(billing.country());
|
||||
|
||||
var terms = customer.paymentTerms();
|
||||
if (terms != null) {
|
||||
entity.setPaymentDueDays(terms.paymentDueDays());
|
||||
entity.setPaymentDescription(terms.description());
|
||||
}
|
||||
|
||||
entity.setStatus(customer.status().name());
|
||||
entity.setCreatedAt(customer.createdAt());
|
||||
entity.setUpdatedAt(customer.updatedAt());
|
||||
|
||||
entity.setDeliveryAddresses(customer.deliveryAddresses().stream()
|
||||
.map(da -> new DeliveryAddressEmbeddable(
|
||||
da.label(),
|
||||
da.address().street(), da.address().houseNumber(),
|
||||
da.address().postalCode(), da.address().city(), da.address().country(),
|
||||
da.contactPerson(), da.deliveryNotes()))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
entity.setPreferences(customer.preferences());
|
||||
|
||||
// FrameContract is saved separately — not via the @OneToOne mapping
|
||||
// (handled in JpaCustomerRepository)
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
public Customer toDomain(CustomerEntity entity, FrameContractEntity fcEntity) {
|
||||
Address billingAddress = new Address(
|
||||
entity.getBillingStreet(), entity.getBillingHouseNumber(),
|
||||
entity.getBillingPostalCode(), entity.getBillingCity(), entity.getBillingCountry()
|
||||
);
|
||||
|
||||
ContactInfo contactInfo = new ContactInfo(
|
||||
entity.getPhone(), entity.getEmail(), entity.getContactPerson()
|
||||
);
|
||||
|
||||
PaymentTerms paymentTerms = null;
|
||||
if (entity.getPaymentDueDays() != null) {
|
||||
paymentTerms = new PaymentTerms(entity.getPaymentDueDays(), entity.getPaymentDescription());
|
||||
}
|
||||
|
||||
List<DeliveryAddress> deliveryAddresses = entity.getDeliveryAddresses().stream()
|
||||
.map(da -> new DeliveryAddress(
|
||||
da.getLabel(),
|
||||
new Address(da.getStreet(), da.getHouseNumber(),
|
||||
da.getPostalCode(), da.getCity(), da.getCountry()),
|
||||
da.getContactPerson(), da.getDeliveryNotes()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
FrameContract frameContract = null;
|
||||
if (fcEntity != null) {
|
||||
List<ContractLineItem> lineItems = fcEntity.getLineItems().stream()
|
||||
.map(li -> new ContractLineItem(
|
||||
ArticleId.of(li.getArticleId()),
|
||||
new Money(li.getAgreedPriceAmount(), Currency.getInstance(li.getAgreedPriceCurrency())),
|
||||
li.getAgreedQuantity(),
|
||||
li.getUnit() != null ? Unit.valueOf(li.getUnit()) : null))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
frameContract = FrameContract.reconstitute(
|
||||
FrameContractId.of(fcEntity.getId()),
|
||||
fcEntity.getValidFrom(),
|
||||
fcEntity.getValidUntil(),
|
||||
DeliveryRhythm.valueOf(fcEntity.getDeliveryRhythm()),
|
||||
lineItems
|
||||
);
|
||||
}
|
||||
|
||||
return Customer.reconstitute(
|
||||
CustomerId.of(entity.getId()),
|
||||
new CustomerName(entity.getName()),
|
||||
CustomerType.valueOf(entity.getType()),
|
||||
billingAddress,
|
||||
contactInfo,
|
||||
paymentTerms,
|
||||
deliveryAddresses,
|
||||
frameContract,
|
||||
entity.getPreferences(),
|
||||
CustomerStatus.valueOf(entity.getStatus()),
|
||||
entity.getCreatedAt(),
|
||||
entity.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public FrameContractEntity toFrameContractEntity(FrameContract fc, String customerId) {
|
||||
var entity = new FrameContractEntity(
|
||||
fc.id().value(), customerId,
|
||||
fc.validFrom(), fc.validUntil(),
|
||||
fc.deliveryRhythm().name()
|
||||
);
|
||||
entity.setLineItems(fc.lineItems().stream()
|
||||
.map(li -> new ContractLineItemEmbeddable(
|
||||
li.articleId().value(),
|
||||
li.agreedPrice().amount(),
|
||||
li.agreedPrice().currency().getCurrencyCode(),
|
||||
li.agreedQuantity(),
|
||||
li.unit() != null ? li.unit().name() : null))
|
||||
.collect(Collectors.toList()));
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.repository;
|
||||
|
||||
import de.effigenix.infrastructure.masterdata.persistence.entity.CustomerEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CustomerJpaRepository extends JpaRepository<CustomerEntity, String> {
|
||||
|
||||
List<CustomerEntity> findByType(String type);
|
||||
|
||||
List<CustomerEntity> findByStatus(String status);
|
||||
|
||||
boolean existsByName(String name);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.repository;
|
||||
|
||||
import de.effigenix.infrastructure.masterdata.persistence.entity.FrameContractEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FrameContractJpaRepository extends JpaRepository<FrameContractEntity, String> {
|
||||
|
||||
Optional<FrameContractEntity> findByCustomerId(String customerId);
|
||||
|
||||
void deleteByCustomerId(String customerId);
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package de.effigenix.infrastructure.masterdata.persistence.repository;
|
||||
|
||||
import de.effigenix.domain.masterdata.*;
|
||||
import de.effigenix.infrastructure.masterdata.persistence.entity.FrameContractEntity;
|
||||
import de.effigenix.infrastructure.masterdata.persistence.mapper.CustomerMapper;
|
||||
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 JpaCustomerRepository implements CustomerRepository {
|
||||
|
||||
private final CustomerJpaRepository jpaRepository;
|
||||
private final FrameContractJpaRepository frameContractJpaRepository;
|
||||
private final CustomerMapper mapper;
|
||||
|
||||
public JpaCustomerRepository(CustomerJpaRepository jpaRepository,
|
||||
FrameContractJpaRepository frameContractJpaRepository,
|
||||
CustomerMapper mapper) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
this.frameContractJpaRepository = frameContractJpaRepository;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Optional<Customer>> findById(CustomerId id) {
|
||||
try {
|
||||
var entityOpt = jpaRepository.findById(id.value());
|
||||
if (entityOpt.isEmpty()) {
|
||||
return Result.success(Optional.empty());
|
||||
}
|
||||
var fcEntity = frameContractJpaRepository.findByCustomerId(id.value()).orElse(null);
|
||||
return Result.success(Optional.of(mapper.toDomain(entityOpt.get(), fcEntity)));
|
||||
} catch (Exception e) {
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Customer>> findAll() {
|
||||
try {
|
||||
List<Customer> result = jpaRepository.findAll().stream()
|
||||
.map(entity -> {
|
||||
var fc = frameContractJpaRepository.findByCustomerId(entity.getId()).orElse(null);
|
||||
return mapper.toDomain(entity, fc);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Customer>> findByType(CustomerType type) {
|
||||
try {
|
||||
List<Customer> result = jpaRepository.findByType(type.name()).stream()
|
||||
.map(entity -> {
|
||||
var fc = frameContractJpaRepository.findByCustomerId(entity.getId()).orElse(null);
|
||||
return mapper.toDomain(entity, fc);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Customer>> findByStatus(CustomerStatus status) {
|
||||
try {
|
||||
List<Customer> result = jpaRepository.findByStatus(status.name()).stream()
|
||||
.map(entity -> {
|
||||
var fc = frameContractJpaRepository.findByCustomerId(entity.getId()).orElse(null);
|
||||
return mapper.toDomain(entity, fc);
|
||||
})
|
||||
.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(Customer customer) {
|
||||
try {
|
||||
jpaRepository.save(mapper.toEntity(customer));
|
||||
|
||||
// Handle FrameContract separately
|
||||
frameContractJpaRepository.deleteByCustomerId(customer.id().value());
|
||||
if (customer.frameContract() != null) {
|
||||
var fcEntity = mapper.toFrameContractEntity(customer.frameContract(), customer.id().value());
|
||||
frameContractJpaRepository.save(fcEntity);
|
||||
}
|
||||
|
||||
return Result.success(null);
|
||||
} catch (Exception e) {
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Result<RepositoryError, Void> delete(Customer customer) {
|
||||
try {
|
||||
frameContractJpaRepository.deleteByCustomerId(customer.id().value());
|
||||
jpaRepository.deleteById(customer.id().value());
|
||||
return Result.success(null);
|
||||
} catch (Exception e) {
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Boolean> existsByName(CustomerName name) {
|
||||
try {
|
||||
return Result.success(jpaRepository.existsByName(name.value()));
|
||||
} catch (Exception e) {
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.controller;
|
||||
|
||||
import de.effigenix.application.masterdata.*;
|
||||
import de.effigenix.application.masterdata.command.*;
|
||||
import de.effigenix.domain.masterdata.Customer;
|
||||
import de.effigenix.domain.masterdata.CustomerError;
|
||||
import de.effigenix.domain.masterdata.CustomerId;
|
||||
import de.effigenix.domain.masterdata.CustomerStatus;
|
||||
import de.effigenix.domain.masterdata.CustomerType;
|
||||
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;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/customers")
|
||||
@SecurityRequirement(name = "Bearer Authentication")
|
||||
@Tag(name = "Customers", description = "Customer management endpoints")
|
||||
public class CustomerController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CustomerController.class);
|
||||
|
||||
private final CreateCustomer createCustomer;
|
||||
private final UpdateCustomer updateCustomer;
|
||||
private final GetCustomer getCustomer;
|
||||
private final ListCustomers listCustomers;
|
||||
private final ActivateCustomer activateCustomer;
|
||||
private final DeactivateCustomer deactivateCustomer;
|
||||
private final AddDeliveryAddress addDeliveryAddress;
|
||||
private final RemoveDeliveryAddress removeDeliveryAddress;
|
||||
private final SetFrameContract setFrameContract;
|
||||
private final RemoveFrameContract removeFrameContract;
|
||||
private final SetPreferences setPreferences;
|
||||
|
||||
public CustomerController(
|
||||
CreateCustomer createCustomer,
|
||||
UpdateCustomer updateCustomer,
|
||||
GetCustomer getCustomer,
|
||||
ListCustomers listCustomers,
|
||||
ActivateCustomer activateCustomer,
|
||||
DeactivateCustomer deactivateCustomer,
|
||||
AddDeliveryAddress addDeliveryAddress,
|
||||
RemoveDeliveryAddress removeDeliveryAddress,
|
||||
SetFrameContract setFrameContract,
|
||||
RemoveFrameContract removeFrameContract,
|
||||
SetPreferences setPreferences
|
||||
) {
|
||||
this.createCustomer = createCustomer;
|
||||
this.updateCustomer = updateCustomer;
|
||||
this.getCustomer = getCustomer;
|
||||
this.listCustomers = listCustomers;
|
||||
this.activateCustomer = activateCustomer;
|
||||
this.deactivateCustomer = deactivateCustomer;
|
||||
this.addDeliveryAddress = addDeliveryAddress;
|
||||
this.removeDeliveryAddress = removeDeliveryAddress;
|
||||
this.setFrameContract = setFrameContract;
|
||||
this.removeFrameContract = removeFrameContract;
|
||||
this.setPreferences = setPreferences;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Customer> createCustomer(
|
||||
@Valid @RequestBody CreateCustomerRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Creating customer: {} by actor: {}", request.name(), actorId.value());
|
||||
|
||||
var cmd = new CreateCustomerCommand(
|
||||
request.name(), request.type(),
|
||||
request.street(), request.houseNumber(), request.postalCode(),
|
||||
request.city(), request.country(),
|
||||
request.phone(), request.email(), request.contactPerson(),
|
||||
request.paymentDueDays(), request.paymentDescription()
|
||||
);
|
||||
var result = createCustomer.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Customer created: {}", request.name());
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Customer>> listCustomers(
|
||||
@RequestParam(value = "type", required = false) CustomerType type,
|
||||
@RequestParam(value = "status", required = false) CustomerStatus status,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Listing customers by actor: {}", actorId.value());
|
||||
|
||||
Result<CustomerError, List<Customer>> result;
|
||||
if (type != null) {
|
||||
result = listCustomers.executeByType(type);
|
||||
} else if (status != null) {
|
||||
result = listCustomers.executeByStatus(status);
|
||||
} else {
|
||||
result = listCustomers.execute();
|
||||
}
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Customer> getCustomer(
|
||||
@PathVariable("id") String customerId,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Getting customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var result = getCustomer.execute(CustomerId.of(customerId));
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Customer> updateCustomer(
|
||||
@PathVariable("id") String customerId,
|
||||
@Valid @RequestBody UpdateCustomerRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Updating customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var cmd = new UpdateCustomerCommand(
|
||||
customerId,
|
||||
request.name(),
|
||||
request.street(), request.houseNumber(), request.postalCode(),
|
||||
request.city(), request.country(),
|
||||
request.phone(), request.email(), request.contactPerson(),
|
||||
request.paymentDueDays(), request.paymentDescription()
|
||||
);
|
||||
var result = updateCustomer.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Customer updated: {}", customerId);
|
||||
return ResponseEntity.ok(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/activate")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Customer> activate(
|
||||
@PathVariable("id") String customerId,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Activating customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var result = activateCustomer.execute(CustomerId.of(customerId), actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Customer activated: {}", customerId);
|
||||
return ResponseEntity.ok(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/deactivate")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Customer> deactivate(
|
||||
@PathVariable("id") String customerId,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Deactivating customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var result = deactivateCustomer.execute(CustomerId.of(customerId), actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Customer deactivated: {}", customerId);
|
||||
return ResponseEntity.ok(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delivery-addresses")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Customer> addDeliveryAddress(
|
||||
@PathVariable("id") String customerId,
|
||||
@Valid @RequestBody AddDeliveryAddressRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Adding delivery address to customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var cmd = new AddDeliveryAddressCommand(
|
||||
customerId, request.label(),
|
||||
request.street(), request.houseNumber(), request.postalCode(),
|
||||
request.city(), request.country(),
|
||||
request.contactPerson(), request.deliveryNotes()
|
||||
);
|
||||
var result = addDeliveryAddress.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Delivery address added to customer: {}", customerId);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/delivery-addresses/{label}")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Void> removeDeliveryAddress(
|
||||
@PathVariable("id") String customerId,
|
||||
@PathVariable("label") String label,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Removing delivery address '{}' from customer: {} by actor: {}", label, customerId, actorId.value());
|
||||
|
||||
var cmd = new RemoveDeliveryAddressCommand(customerId, label);
|
||||
var result = removeDeliveryAddress.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Delivery address removed from customer: {}", customerId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/frame-contract")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Customer> setFrameContract(
|
||||
@PathVariable("id") String customerId,
|
||||
@Valid @RequestBody SetFrameContractRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Setting frame contract for customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var lineItems = request.lineItems().stream()
|
||||
.map(li -> new SetFrameContractCommand.LineItem(
|
||||
li.articleId(), li.agreedPrice(), li.agreedQuantity(), li.unit()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
var cmd = new SetFrameContractCommand(
|
||||
customerId, request.validFrom(), request.validUntil(),
|
||||
request.rhythm(), lineItems
|
||||
);
|
||||
var result = setFrameContract.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Frame contract set for customer: {}", customerId);
|
||||
return ResponseEntity.ok(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/frame-contract")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Void> removeFrameContract(
|
||||
@PathVariable("id") String customerId,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Removing frame contract from customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var result = removeFrameContract.execute(CustomerId.of(customerId), actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Frame contract removed from customer: {}", customerId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/preferences")
|
||||
@PreAuthorize("hasAuthority('MASTERDATA_WRITE')")
|
||||
public ResponseEntity<Customer> setPreferences(
|
||||
@PathVariable("id") String customerId,
|
||||
@Valid @RequestBody SetPreferencesRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = extractActorId(authentication);
|
||||
logger.info("Setting preferences for customer: {} by actor: {}", customerId, actorId.value());
|
||||
|
||||
var cmd = new SetPreferencesCommand(customerId, request.preferences());
|
||||
var result = setPreferences.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new CustomerDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
logger.info("Preferences set for customer: {}", customerId);
|
||||
return ResponseEntity.ok(result.unsafeGetValue());
|
||||
}
|
||||
|
||||
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 CustomerDomainErrorException extends RuntimeException {
|
||||
private final CustomerError error;
|
||||
|
||||
public CustomerDomainErrorException(CustomerError error) {
|
||||
super(error.message());
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public CustomerError getError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record AddDeliveryAddressRequest(
|
||||
String label,
|
||||
@NotBlank String street,
|
||||
String houseNumber,
|
||||
@NotBlank String postalCode,
|
||||
@NotBlank String city,
|
||||
@NotBlank String country,
|
||||
String contactPerson,
|
||||
String deliveryNotes
|
||||
) {}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||
|
||||
import de.effigenix.domain.masterdata.CustomerType;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record CreateCustomerRequest(
|
||||
@NotBlank String name,
|
||||
@NotNull CustomerType type,
|
||||
@NotBlank String street,
|
||||
String houseNumber,
|
||||
@NotBlank String postalCode,
|
||||
@NotBlank String city,
|
||||
@NotBlank String country,
|
||||
@NotBlank String phone,
|
||||
String email,
|
||||
String contactPerson,
|
||||
Integer paymentDueDays,
|
||||
String paymentDescription
|
||||
) {}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||
|
||||
import de.effigenix.domain.masterdata.DeliveryRhythm;
|
||||
import de.effigenix.domain.masterdata.Unit;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record SetFrameContractRequest(
|
||||
LocalDate validFrom,
|
||||
LocalDate validUntil,
|
||||
@NotNull DeliveryRhythm rhythm,
|
||||
@NotEmpty List<LineItem> lineItems
|
||||
) {
|
||||
public record LineItem(
|
||||
@NotNull String articleId,
|
||||
@NotNull BigDecimal agreedPrice,
|
||||
BigDecimal agreedQuantity,
|
||||
Unit unit
|
||||
) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||
|
||||
import de.effigenix.domain.masterdata.CustomerPreference;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public record SetPreferencesRequest(
|
||||
@NotNull Set<CustomerPreference> preferences
|
||||
) {}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.dto;
|
||||
|
||||
public record UpdateCustomerRequest(
|
||||
String name,
|
||||
String street,
|
||||
String houseNumber,
|
||||
String postalCode,
|
||||
String city,
|
||||
String country,
|
||||
String phone,
|
||||
String email,
|
||||
String contactPerson,
|
||||
Integer paymentDueDays,
|
||||
String paymentDescription
|
||||
) {}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package de.effigenix.infrastructure.masterdata.web.exception;
|
||||
|
||||
import de.effigenix.domain.masterdata.ArticleError;
|
||||
import de.effigenix.domain.masterdata.CustomerError;
|
||||
import de.effigenix.domain.masterdata.ProductCategoryError;
|
||||
import de.effigenix.domain.masterdata.SupplierError;
|
||||
|
||||
|
|
@ -46,4 +47,17 @@ public final class MasterDataErrorHttpStatusMapper {
|
|||
case ProductCategoryError.RepositoryFailure e -> 500;
|
||||
};
|
||||
}
|
||||
|
||||
public static int toHttpStatus(CustomerError error) {
|
||||
return switch (error) {
|
||||
case CustomerError.CustomerNotFound e -> 404;
|
||||
case CustomerError.CustomerNameAlreadyExists e -> 409;
|
||||
case CustomerError.FrameContractNotAllowed e -> 400;
|
||||
case CustomerError.InvalidFrameContract e -> 400;
|
||||
case CustomerError.DuplicateContractLineItem e -> 409;
|
||||
case CustomerError.ValidationFailure e -> 400;
|
||||
case CustomerError.Unauthorized e -> 403;
|
||||
case CustomerError.RepositoryFailure e -> 500;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.CustomerError;
|
||||
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.CustomerController;
|
||||
import de.effigenix.infrastructure.masterdata.web.controller.ProductCategoryController;
|
||||
import de.effigenix.infrastructure.masterdata.web.controller.SupplierController;
|
||||
import de.effigenix.infrastructure.masterdata.web.exception.MasterDataErrorHttpStatusMapper;
|
||||
|
|
@ -156,6 +158,25 @@ public class GlobalExceptionHandler {
|
|||
return ResponseEntity.status(status).body(errorResponse);
|
||||
}
|
||||
|
||||
@ExceptionHandler(CustomerController.CustomerDomainErrorException.class)
|
||||
public ResponseEntity<ErrorResponse> handleCustomerDomainError(
|
||||
CustomerController.CustomerDomainErrorException ex,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
CustomerError error = ex.getError();
|
||||
int status = MasterDataErrorHttpStatusMapper.toHttpStatus(error);
|
||||
logger.warn("Customer 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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
<?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="007-create-customers-table" author="effigenix">
|
||||
<createTable tableName="customers">
|
||||
<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="type" type="VARCHAR(10)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="phone" type="VARCHAR(50)"/>
|
||||
<column name="email" type="VARCHAR(255)"/>
|
||||
<column name="contact_person" type="VARCHAR(200)"/>
|
||||
<column name="billing_street" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="billing_house_number" type="VARCHAR(20)"/>
|
||||
<column name="billing_postal_code" type="VARCHAR(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="billing_city" type="VARCHAR(100)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="billing_country" type="VARCHAR(2)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="payment_due_days" type="INT"/>
|
||||
<column name="payment_description" type="VARCHAR(500)"/>
|
||||
<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 customers ADD CONSTRAINT chk_customer_type CHECK (type IN ('B2C', 'B2B'));
|
||||
ALTER TABLE customers ADD CONSTRAINT chk_customer_status CHECK (status IN ('ACTIVE', 'INACTIVE'));
|
||||
</sql>
|
||||
<createIndex tableName="customers" indexName="idx_customers_name">
|
||||
<column name="name"/>
|
||||
</createIndex>
|
||||
<createIndex tableName="customers" indexName="idx_customers_type">
|
||||
<column name="type"/>
|
||||
</createIndex>
|
||||
<createIndex tableName="customers" indexName="idx_customers_status">
|
||||
<column name="status"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="007-create-delivery-addresses-table" author="effigenix">
|
||||
<createTable tableName="delivery_addresses">
|
||||
<column name="customer_id" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="label" type="VARCHAR(100)"/>
|
||||
<column name="street" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="house_number" type="VARCHAR(20)"/>
|
||||
<column name="postal_code" type="VARCHAR(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="city" type="VARCHAR(100)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="country" type="VARCHAR(2)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="contact_person" type="VARCHAR(200)"/>
|
||||
<column name="delivery_notes" type="VARCHAR(500)"/>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseTableName="delivery_addresses" baseColumnNames="customer_id"
|
||||
referencedTableName="customers" referencedColumnNames="id"
|
||||
constraintName="fk_delivery_addresses_customer" onDelete="CASCADE"/>
|
||||
<createIndex tableName="delivery_addresses" indexName="idx_delivery_addresses_customer_id">
|
||||
<column name="customer_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="007-create-customer-preferences-table" author="effigenix">
|
||||
<createTable tableName="customer_preferences">
|
||||
<column name="customer_id" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="preference" type="VARCHAR(30)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<addPrimaryKey tableName="customer_preferences" columnNames="customer_id, preference"/>
|
||||
<addForeignKeyConstraint baseTableName="customer_preferences" baseColumnNames="customer_id"
|
||||
referencedTableName="customers" referencedColumnNames="id"
|
||||
constraintName="fk_customer_preferences_customer" onDelete="CASCADE"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="007-create-frame-contracts-table" author="effigenix">
|
||||
<createTable tableName="frame_contracts">
|
||||
<column name="id" type="VARCHAR(36)">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="customer_id" type="VARCHAR(36)">
|
||||
<constraints nullable="false" unique="true"/>
|
||||
</column>
|
||||
<column name="valid_from" type="DATE"/>
|
||||
<column name="valid_until" type="DATE"/>
|
||||
<column name="delivery_rhythm" type="VARCHAR(20)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseTableName="frame_contracts" baseColumnNames="customer_id"
|
||||
referencedTableName="customers" referencedColumnNames="id"
|
||||
constraintName="fk_frame_contracts_customer" onDelete="CASCADE"/>
|
||||
<createIndex tableName="frame_contracts" indexName="idx_frame_contracts_customer_id">
|
||||
<column name="customer_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="007-create-contract-line-items-table" author="effigenix">
|
||||
<createTable tableName="contract_line_items">
|
||||
<column name="frame_contract_id" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="article_id" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="agreed_price_amount" type="DECIMAL(19,2)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="agreed_price_currency" type="VARCHAR(3)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="agreed_quantity" type="DECIMAL(19,4)"/>
|
||||
<column name="unit" type="VARCHAR(30)"/>
|
||||
</createTable>
|
||||
<addPrimaryKey tableName="contract_line_items" columnNames="frame_contract_id, article_id"/>
|
||||
<addForeignKeyConstraint baseTableName="contract_line_items" baseColumnNames="frame_contract_id"
|
||||
referencedTableName="frame_contracts" referencedColumnNames="id"
|
||||
constraintName="fk_contract_line_items_contract" onDelete="CASCADE"/>
|
||||
<createIndex tableName="contract_line_items" indexName="idx_contract_line_items_contract_id">
|
||||
<column name="frame_contract_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
@ -11,5 +11,6 @@
|
|||
<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"/>
|
||||
<include file="db/changelog/changes/007-create-customer-schema.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue