mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 12:19: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.application.masterdata.*;
|
||||||
import de.effigenix.domain.masterdata.ArticleRepository;
|
import de.effigenix.domain.masterdata.ArticleRepository;
|
||||||
|
import de.effigenix.domain.masterdata.CustomerRepository;
|
||||||
import de.effigenix.domain.masterdata.ProductCategoryRepository;
|
import de.effigenix.domain.masterdata.ProductCategoryRepository;
|
||||||
import de.effigenix.domain.masterdata.SupplierRepository;
|
import de.effigenix.domain.masterdata.SupplierRepository;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
@ -138,4 +139,61 @@ public class MasterDataUseCaseConfiguration {
|
||||||
public RemoveCertificate removeCertificate(SupplierRepository supplierRepository) {
|
public RemoveCertificate removeCertificate(SupplierRepository supplierRepository) {
|
||||||
return new RemoveCertificate(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;
|
package de.effigenix.infrastructure.masterdata.web.exception;
|
||||||
|
|
||||||
import de.effigenix.domain.masterdata.ArticleError;
|
import de.effigenix.domain.masterdata.ArticleError;
|
||||||
|
import de.effigenix.domain.masterdata.CustomerError;
|
||||||
import de.effigenix.domain.masterdata.ProductCategoryError;
|
import de.effigenix.domain.masterdata.ProductCategoryError;
|
||||||
import de.effigenix.domain.masterdata.SupplierError;
|
import de.effigenix.domain.masterdata.SupplierError;
|
||||||
|
|
||||||
|
|
@ -46,4 +47,17 @@ public final class MasterDataErrorHttpStatusMapper {
|
||||||
case ProductCategoryError.RepositoryFailure e -> 500;
|
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.ArticleError;
|
||||||
import de.effigenix.domain.masterdata.ProductCategoryError;
|
import de.effigenix.domain.masterdata.ProductCategoryError;
|
||||||
|
import de.effigenix.domain.masterdata.CustomerError;
|
||||||
import de.effigenix.domain.masterdata.SupplierError;
|
import de.effigenix.domain.masterdata.SupplierError;
|
||||||
import de.effigenix.domain.usermanagement.UserError;
|
import de.effigenix.domain.usermanagement.UserError;
|
||||||
import de.effigenix.infrastructure.masterdata.web.controller.ArticleController;
|
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.ProductCategoryController;
|
||||||
import de.effigenix.infrastructure.masterdata.web.controller.SupplierController;
|
import de.effigenix.infrastructure.masterdata.web.controller.SupplierController;
|
||||||
import de.effigenix.infrastructure.masterdata.web.exception.MasterDataErrorHttpStatusMapper;
|
import de.effigenix.infrastructure.masterdata.web.exception.MasterDataErrorHttpStatusMapper;
|
||||||
|
|
@ -156,6 +158,25 @@ public class GlobalExceptionHandler {
|
||||||
return ResponseEntity.status(status).body(errorResponse);
|
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.
|
* 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/004-seed-admin-user.xml"/>
|
||||||
<include file="db/changelog/changes/005-create-masterdata-schema.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/006-create-supplier-schema.xml"/>
|
||||||
|
<include file="db/changelog/changes/007-create-customer-schema.xml"/>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue