1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 06:29:35 +01:00
effigenix/backend/src/test/java/de/effigenix/application/masterdata/CustomerUseCaseTest.java
Sebastian Frick 72979c9537 feat: Paginierung für alle GET-List-Endpoints (#61)
Einheitliches Paginierungs-Pattern mit page, size und Multi-Field sort
für alle 14 List-Endpoints. Response-Format ändert sich von [...] zu
{ content: [...], page: { number, size, totalElements, totalPages } }.

Backend:
- Shared Kernel: Page<T>, PageRequest, SortField, SortDirection
- PaginationHelper (SQL ORDER BY mit Whitelist), PageResponse DTO
- Paginated Methoden in allen 14 Domain-Repos + JDBC-Implementierungen
- Safety-Limit (500) für findAllBelowMinimumLevel/ExpiryRelevantBatches
- Alle List-Use-Cases akzeptieren PageRequest, liefern Page<T>
- Alle Controller mit page/size/sort Query-Params + PageResponse

Frontend:
- PagedResponse<T> Type auf nested page-Format aktualisiert
- Alle 14 API-Client-Resourcen liefern PagedResponse mit PaginationParams
- Alle Hooks mit Pagination-State (currentPage, totalPages, pageSize)
- Alle List-Screens mit Seiten-Navigation (Pfeiltasten) und Footer

Loadtest:
- Podman-Support im justfile (DOCKER_HOST auto-detect)
- Verschärfte Performance-Schwellwerte basierend auf Ist-Werten
2026-03-20 16:33:20 +01:00

1119 lines
47 KiB
Java

package de.effigenix.application.masterdata;
import de.effigenix.application.masterdata.customer.*;
import de.effigenix.application.masterdata.customer.command.*;
import de.effigenix.domain.masterdata.*;
import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.domain.masterdata.customer.*;
import de.effigenix.shared.common.Address;
import de.effigenix.shared.common.ContactInfo;
import de.effigenix.shared.common.Money;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.PaymentTerms;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.persistence.UnitOfWork;
import de.effigenix.shared.security.ActorId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("Customer Use Cases")
class CustomerUseCaseTest {
@Mock private CustomerRepository customerRepository;
@Mock private UnitOfWork unitOfWork;
private ActorId performedBy;
@BeforeEach
void setUp() {
lenient().when(unitOfWork.executeAtomically(any()))
.thenAnswer(inv -> ((Supplier<?>)inv.getArgument(0)).get());
performedBy = ActorId.of("admin-user");
}
// ==================== Helpers ====================
private static Customer existingB2BCustomer(String id) {
return Customer.reconstitute(
CustomerId.of(id),
new CustomerName("Metzgerei Müller"),
CustomerType.B2B,
new Address("Hauptstr.", "10", "80331", "München", "DE"),
new ContactInfo("+49 89 12345", "info@mueller.de", "Hans Müller"),
new PaymentTerms(30, "30 Tage netto"),
List.of(),
null,
Set.of(),
CustomerStatus.ACTIVE,
OffsetDateTime.now(ZoneOffset.UTC).minusDays(10),
OffsetDateTime.now(ZoneOffset.UTC).minusDays(1)
);
}
private static Customer existingB2CCustomer(String id) {
return Customer.reconstitute(
CustomerId.of(id),
new CustomerName("Max Mustermann"),
CustomerType.B2C,
new Address("Berliner Str.", "5", "10115", "Berlin", "DE"),
new ContactInfo("+49 30 98765", null, null),
null,
List.of(),
null,
Set.of(),
CustomerStatus.ACTIVE,
OffsetDateTime.now(ZoneOffset.UTC).minusDays(5),
OffsetDateTime.now(ZoneOffset.UTC).minusDays(1)
);
}
private static Customer inactiveCustomer(String id) {
return Customer.reconstitute(
CustomerId.of(id),
new CustomerName("Alte Bäckerei"),
CustomerType.B2B,
new Address("Bahnhofstr.", "1", "60329", "Frankfurt", "DE"),
new ContactInfo("+49 69 11111", null, null),
null,
List.of(),
null,
Set.of(),
CustomerStatus.INACTIVE,
OffsetDateTime.now(ZoneOffset.UTC).minusDays(30),
OffsetDateTime.now(ZoneOffset.UTC).minusDays(2)
);
}
private static CreateCustomerCommand validCreateCommand() {
return new CreateCustomerCommand(
"Neuer Kunde GmbH", CustomerType.B2B,
"Industriestr.", "42", "70173", "Stuttgart", "DE",
"+49 711 55555", "kontakt@neuer-kunde.de", "Anna Schmidt",
14, "14 Tage netto"
);
}
// ==================== CreateCustomer ====================
@Nested
@DisplayName("CreateCustomer")
class CreateCustomerTests {
private CreateCustomer createCustomer;
@BeforeEach
void setUp() {
createCustomer = new CreateCustomer(customerRepository, unitOfWork);
}
@Test
@DisplayName("should create customer successfully")
void shouldCreateCustomerSuccessfully() {
var cmd = validCreateCommand();
when(customerRepository.existsByName(any())).thenReturn(Result.success(false));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = createCustomer.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
var customer = result.unsafeGetValue();
assertThat(customer.name().value()).isEqualTo("Neuer Kunde GmbH");
assertThat(customer.type()).isEqualTo(CustomerType.B2B);
assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
assertThat(customer.billingAddress().street()).isEqualTo("Industriestr.");
assertThat(customer.contactInfo().phone()).isEqualTo("+49 711 55555");
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should fail when customer name already exists")
void shouldFailWhenNameAlreadyExists() {
var cmd = validCreateCommand();
when(customerRepository.existsByName(any())).thenReturn(Result.success(true));
var result = createCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNameAlreadyExists.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ValidationFailure when name is blank")
void shouldFailWhenNameIsBlank() {
var cmd = new CreateCustomerCommand(
"", CustomerType.B2B,
"Str.", null, "12345", "City", "DE",
"+49 1", null, null, null, null
);
var result = createCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when existsByName fails")
void shouldFailWhenExistsByNameFails() {
var cmd = validCreateCommand();
when(customerRepository.existsByName(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = createCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var cmd = validCreateCommand();
when(customerRepository.existsByName(any())).thenReturn(Result.success(false));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = createCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should create customer without optional payment terms")
void shouldCreateWithoutPaymentTerms() {
var cmd = new CreateCustomerCommand(
"Einfach GmbH", CustomerType.B2C,
"Str.", null, "12345", "City", "DE",
"+49 1", null, null, null, null
);
when(customerRepository.existsByName(any())).thenReturn(Result.success(false));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = createCustomer.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().paymentTerms()).isNull();
}
}
// ==================== UpdateCustomer ====================
@Nested
@DisplayName("UpdateCustomer")
class UpdateCustomerTests {
private UpdateCustomer updateCustomer;
@BeforeEach
void setUp() {
updateCustomer = new UpdateCustomer(customerRepository, unitOfWork);
}
@Test
@DisplayName("should update customer successfully")
void shouldUpdateCustomerSuccessfully() {
var customerId = "cust-1";
var cmd = new UpdateCustomerCommand(
customerId, "Neuer Name", "Neue Str.", "99", "99999", "Neustadt", "DE",
"+49 999 0000", "neu@test.de", "Neue Person", 60, "60 Tage netto"
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = updateCustomer.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var cmd = new UpdateCustomerCommand(
"nonexistent", null, null, null, null, null, null,
null, null, null, null, null
);
when(customerRepository.findById(CustomerId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var result = updateCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var cmd = new UpdateCustomerCommand(
"cust-1", null, null, null, null, null, null,
null, null, null, null, null
);
when(customerRepository.findById(CustomerId.of("cust-1")))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));
var result = updateCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = "cust-1";
var cmd = new UpdateCustomerCommand(
customerId, "Updated", null, null, null, null, null,
null, null, null, null, null
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = updateCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with ValidationFailure when updated name is blank")
void shouldFailWhenUpdatedNameIsBlank() {
var customerId = "cust-1";
var cmd = new UpdateCustomerCommand(
customerId, "", null, null, null, null, null,
null, null, null, null, null
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
var result = updateCustomer.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
verify(customerRepository, never()).save(any());
}
}
// ==================== GetCustomer ====================
@Nested
@DisplayName("GetCustomer")
class GetCustomerTests {
private GetCustomer getCustomer;
@BeforeEach
void setUp() {
getCustomer = new GetCustomer(customerRepository);
}
@Test
@DisplayName("should return customer when found")
void shouldReturnCustomerWhenFound() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
var result = getCustomer.execute(customerId);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().id()).isEqualTo(customerId);
}
@Test
@DisplayName("should fail with CustomerNotFound when not found")
void shouldFailWhenNotFound() {
var customerId = CustomerId.of("nonexistent");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.empty()));
var result = getCustomer.execute(customerId);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when repository fails")
void shouldFailWhenRepositoryFails() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = getCustomer.execute(customerId);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
// ==================== ListCustomers ====================
@Nested
@DisplayName("ListCustomers")
class ListCustomersTests {
private ListCustomers listCustomers;
@BeforeEach
void setUp() {
listCustomers = new ListCustomers(customerRepository);
}
@Test
@DisplayName("should return all customers")
void shouldReturnAllCustomers() {
var customers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2"));
var pageRequest = PageRequest.of(0, 100);
when(customerRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.of(customers, 0, 100, 2)));
var result = listCustomers.execute(pageRequest);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().content()).hasSize(2);
}
@Test
@DisplayName("should return empty list when no customers exist")
void shouldReturnEmptyList() {
var pageRequest = PageRequest.of(0, 100);
when(customerRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.empty(100)));
var result = listCustomers.execute(pageRequest);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().content()).isEmpty();
}
@Test
@DisplayName("should fail with RepositoryFailure when findAll fails")
void shouldFailWhenFindAllFails() {
var pageRequest = PageRequest.of(0, 100);
when(customerRepository.findAll(pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));
var result = listCustomers.execute(pageRequest);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
// ==================== ActivateCustomer ====================
@Nested
@DisplayName("ActivateCustomer")
class ActivateCustomerTests {
private ActivateCustomer activateCustomer;
@BeforeEach
void setUp() {
activateCustomer = new ActivateCustomer(customerRepository, unitOfWork);
}
@Test
@DisplayName("should activate an inactive customer")
void shouldActivateInactiveCustomer() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(inactiveCustomer("cust-1"))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = activateCustomer.execute(customerId, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(CustomerStatus.ACTIVE);
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var customerId = CustomerId.of("nonexistent");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.empty()));
var result = activateCustomer.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = activateCustomer.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(inactiveCustomer("cust-1"))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = activateCustomer.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
// ==================== DeactivateCustomer ====================
@Nested
@DisplayName("DeactivateCustomer")
class DeactivateCustomerTests {
private DeactivateCustomer deactivateCustomer;
@BeforeEach
void setUp() {
deactivateCustomer = new DeactivateCustomer(customerRepository, unitOfWork);
}
@Test
@DisplayName("should deactivate an active customer")
void shouldDeactivateActiveCustomer() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = deactivateCustomer.execute(customerId, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(CustomerStatus.INACTIVE);
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var customerId = CustomerId.of("nonexistent");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.empty()));
var result = deactivateCustomer.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = deactivateCustomer.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = deactivateCustomer.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
// ==================== AddDeliveryAddress ====================
@Nested
@DisplayName("AddDeliveryAddress")
class AddDeliveryAddressTests {
private AddDeliveryAddress addDeliveryAddress;
@BeforeEach
void setUp() {
addDeliveryAddress = new AddDeliveryAddress(customerRepository, unitOfWork);
}
@Test
@DisplayName("should add delivery address successfully")
void shouldAddDeliveryAddressSuccessfully() {
var customerId = "cust-1";
var cmd = new AddDeliveryAddressCommand(
customerId, "Lager Süd", "Lagerstr.", "1", "80000", "München", "DE",
"Max Lager", "Tor 3 benutzen"
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = addDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().deliveryAddresses()).hasSize(1);
assertThat(result.unsafeGetValue().deliveryAddresses().getFirst().label()).isEqualTo("Lager Süd");
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var cmd = new AddDeliveryAddressCommand(
"nonexistent", "Label", "Str.", null, "12345", "City", "DE", null, null
);
when(customerRepository.findById(CustomerId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var result = addDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ValidationFailure when address is invalid")
void shouldFailWhenAddressIsInvalid() {
var customerId = "cust-1";
var cmd = new AddDeliveryAddressCommand(
customerId, "Label", "", null, "", "", "XX",
null, null
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
var result = addDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var cmd = new AddDeliveryAddressCommand(
"cust-1", "Label", "Str.", null, "12345", "City", "DE", null, null
);
when(customerRepository.findById(CustomerId.of("cust-1")))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));
var result = addDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = "cust-1";
var cmd = new AddDeliveryAddressCommand(
customerId, "Label", "Str.", null, "12345", "City", "DE", null, null
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = addDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
// ==================== RemoveDeliveryAddress ====================
@Nested
@DisplayName("RemoveDeliveryAddress")
class RemoveDeliveryAddressTests {
private RemoveDeliveryAddress removeDeliveryAddress;
@BeforeEach
void setUp() {
removeDeliveryAddress = new RemoveDeliveryAddress(customerRepository, unitOfWork);
}
@Test
@DisplayName("should remove delivery address successfully")
void shouldRemoveDeliveryAddressSuccessfully() {
var customerId = "cust-1";
var customerWithAddr = Customer.reconstitute(
CustomerId.of(customerId),
new CustomerName("Metzgerei Müller"),
CustomerType.B2B,
new Address("Hauptstr.", "10", "80331", "München", "DE"),
new ContactInfo("+49 89 12345", null, null),
null,
List.of(new DeliveryAddress("Lager Süd",
new Address("Lagerstr.", "1", "80000", "München", "DE"), null, null)),
null, Set.of(), CustomerStatus.ACTIVE,
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
var cmd = new RemoveDeliveryAddressCommand(customerId, "Lager Süd");
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(customerWithAddr)));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = removeDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().deliveryAddresses()).isEmpty();
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var cmd = new RemoveDeliveryAddressCommand("nonexistent", "Label");
when(customerRepository.findById(CustomerId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var result = removeDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var cmd = new RemoveDeliveryAddressCommand("cust-1", "Label");
when(customerRepository.findById(CustomerId.of("cust-1")))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));
var result = removeDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = "cust-1";
var cmd = new RemoveDeliveryAddressCommand(customerId, "Label");
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = removeDeliveryAddress.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
// ==================== SetPreferences ====================
@Nested
@DisplayName("SetPreferences")
class SetPreferencesTests {
private SetPreferences setPreferences;
@BeforeEach
void setUp() {
setPreferences = new SetPreferences(customerRepository, unitOfWork);
}
@Test
@DisplayName("should set preferences successfully")
void shouldSetPreferencesSuccessfully() {
var customerId = "cust-1";
var prefs = Set.of(CustomerPreference.BIO, CustomerPreference.REGIONAL);
var cmd = new SetPreferencesCommand(customerId, prefs);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = setPreferences.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().preferences())
.containsExactlyInAnyOrder(CustomerPreference.BIO, CustomerPreference.REGIONAL);
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should clear preferences with empty set")
void shouldClearPreferencesWithEmptySet() {
var customerId = "cust-1";
var cmd = new SetPreferencesCommand(customerId, Set.of());
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = setPreferences.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().preferences()).isEmpty();
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var cmd = new SetPreferencesCommand("nonexistent", Set.of(CustomerPreference.BIO));
when(customerRepository.findById(CustomerId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var result = setPreferences.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var cmd = new SetPreferencesCommand("cust-1", Set.of());
when(customerRepository.findById(CustomerId.of("cust-1")))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));
var result = setPreferences.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = "cust-1";
var cmd = new SetPreferencesCommand(customerId, Set.of(CustomerPreference.HALAL));
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = setPreferences.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
// ==================== SetFrameContract ====================
@Nested
@DisplayName("SetFrameContract")
class SetFrameContractTests {
private SetFrameContract setFrameContract;
@BeforeEach
void setUp() {
setFrameContract = new SetFrameContract(customerRepository, unitOfWork);
}
private SetFrameContractCommand validFrameContractCommand(String customerId) {
return new SetFrameContractCommand(
customerId,
LocalDate.now(),
LocalDate.now().plusMonths(12),
DeliveryRhythm.WEEKLY,
List.of(new SetFrameContractCommand.LineItem(
"article-1", new BigDecimal("9.99"), new BigDecimal("100"), Unit.KG
))
);
}
@Test
@DisplayName("should set frame contract on B2B customer successfully")
void shouldSetFrameContractOnB2BCustomer() {
var customerId = "cust-1";
var cmd = validFrameContractCommand(customerId);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().frameContract()).isNotNull();
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should fail with FrameContractNotAllowed when customer is B2C")
void shouldFailWhenCustomerIsB2C() {
var customerId = "cust-b2c";
var cmd = validFrameContractCommand(customerId);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2CCustomer(customerId))));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.FrameContractNotAllowed.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var cmd = validFrameContractCommand("nonexistent");
when(customerRepository.findById(CustomerId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with InvalidFrameContract when validUntil is before validFrom")
void shouldFailWhenDatesInvalid() {
var customerId = "cust-1";
var cmd = new SetFrameContractCommand(
customerId,
LocalDate.now().plusMonths(12),
LocalDate.now(), // before validFrom
DeliveryRhythm.WEEKLY,
List.of(new SetFrameContractCommand.LineItem(
"article-1", new BigDecimal("9.99"), new BigDecimal("100"), Unit.KG
))
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.InvalidFrameContract.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with InvalidFrameContract when line items are empty")
void shouldFailWhenLineItemsEmpty() {
var customerId = "cust-1";
var cmd = new SetFrameContractCommand(
customerId, LocalDate.now(), LocalDate.now().plusMonths(6),
DeliveryRhythm.MONTHLY, List.of()
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.InvalidFrameContract.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var cmd = validFrameContractCommand("cust-1");
when(customerRepository.findById(CustomerId.of("cust-1")))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = "cust-1";
var cmd = validFrameContractCommand(customerId);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with ValidationFailure when price is negative")
void shouldFailWhenPriceIsNegative() {
var customerId = "cust-1";
var cmd = new SetFrameContractCommand(
customerId, LocalDate.now(), LocalDate.now().plusMonths(6),
DeliveryRhythm.MONTHLY,
List.of(new SetFrameContractCommand.LineItem(
"article-1", new BigDecimal("-5.00"), new BigDecimal("100"), Unit.KG
))
);
when(customerRepository.findById(CustomerId.of(customerId)))
.thenReturn(Result.success(Optional.of(existingB2BCustomer(customerId))));
var result = setFrameContract.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
verify(customerRepository, never()).save(any());
}
}
// ==================== RemoveFrameContract ====================
@Nested
@DisplayName("RemoveFrameContract")
class RemoveFrameContractTests {
private RemoveFrameContract removeFrameContract;
@BeforeEach
void setUp() {
removeFrameContract = new RemoveFrameContract(customerRepository, unitOfWork);
}
@Test
@DisplayName("should remove frame contract successfully")
void shouldRemoveFrameContractSuccessfully() {
var customerId = CustomerId.of("cust-1");
var customerWithContract = existingB2BCustomer("cust-1");
// Set a frame contract on the customer via reconstitute
var contractCustomer = Customer.reconstitute(
customerId,
new CustomerName("Metzgerei Müller"),
CustomerType.B2B,
new Address("Hauptstr.", "10", "80331", "München", "DE"),
new ContactInfo("+49 89 12345", null, null),
null,
List.of(),
FrameContract.reconstitute(
FrameContractId.generate(),
LocalDate.now().minusMonths(1),
LocalDate.now().plusMonths(11),
DeliveryRhythm.WEEKLY,
List.of(new ContractLineItem(
ArticleId.of("a1"), Money.euro(new BigDecimal("10.00")),
new BigDecimal("50"), Unit.KG))
),
Set.of(), CustomerStatus.ACTIVE,
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
);
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(contractCustomer)));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = removeFrameContract.execute(customerId, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().frameContract()).isNull();
verify(customerRepository).save(any(Customer.class));
}
@Test
@DisplayName("should succeed even when no frame contract exists")
void shouldSucceedWhenNoFrameContract() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
when(customerRepository.save(any())).thenReturn(Result.success(null));
var result = removeFrameContract.execute(customerId, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().frameContract()).isNull();
}
@Test
@DisplayName("should fail with CustomerNotFound when customer does not exist")
void shouldFailWhenCustomerNotFound() {
var customerId = CustomerId.of("nonexistent");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.empty()));
var result = removeFrameContract.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.CustomerNotFound.class);
verify(customerRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = removeFrameContract.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var customerId = CustomerId.of("cust-1");
when(customerRepository.findById(customerId))
.thenReturn(Result.success(Optional.of(existingB2BCustomer("cust-1"))));
when(customerRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = removeFrameContract.execute(customerId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
}
}