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

test: Unit-Tests für Masterdata-Domain und Application Layer

Domain-Tests: Article, Customer, ProductCategory, Supplier
Application-Tests: ArticleUseCase, CustomerUseCase,
  ProductCategoryUseCase, SupplierUseCase, ListStorageLocations

JaCoCo: Stub-Paket von Coverage-Analyse ausgeschlossen
This commit is contained in:
Sebastian Frick 2026-02-24 09:42:46 +01:00
parent d7fcc946e7
commit 8a9bf849a9
10 changed files with 6453 additions and 0 deletions

View file

@ -0,0 +1,234 @@
package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
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.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("ListStorageLocations Use Case")
class ListStorageLocationsTest {
@Mock private StorageLocationRepository storageLocationRepository;
private ListStorageLocations listStorageLocations;
@BeforeEach
void setUp() {
listStorageLocations = new ListStorageLocations(storageLocationRepository);
}
private StorageLocation activeLocation(String name, StorageType type) {
return StorageLocation.reconstitute(
StorageLocationId.generate(),
new StorageLocationName(name),
type, null, true
);
}
private StorageLocation inactiveLocation(String name, StorageType type) {
return StorageLocation.reconstitute(
StorageLocationId.generate(),
new StorageLocationName(name),
type, null, false
);
}
// ==================== Ohne Filter ====================
@Nested
@DisplayName("ohne Filter")
class NoFilter {
@Test
@DisplayName("should return all storage locations")
void shouldReturnAll() {
var locations = List.of(
activeLocation("Lager A", StorageType.DRY_STORAGE),
inactiveLocation("Lager B", StorageType.COLD_ROOM)
);
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
var result = listStorageLocations.execute(null, null);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2);
}
@Test
@DisplayName("should return empty list when none exist")
void shouldReturnEmptyList() {
when(storageLocationRepository.findAll()).thenReturn(Result.success(List.of()));
var result = listStorageLocations.execute(null, null);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty();
}
@Test
@DisplayName("should fail when repository fails")
void shouldFailWhenRepositoryFails() {
when(storageLocationRepository.findAll())
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listStorageLocations.execute(null, null);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.RepositoryFailure.class);
}
}
// ==================== Active-Filter ====================
@Nested
@DisplayName("active-Filter")
class ActiveFilter {
@Test
@DisplayName("should return only active locations when active=true")
void shouldReturnOnlyActive() {
var locations = List.of(
activeLocation("Aktiv", StorageType.DRY_STORAGE),
inactiveLocation("Inaktiv", StorageType.COLD_ROOM)
);
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
var result = listStorageLocations.execute(null, true);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Aktiv");
}
@Test
@DisplayName("should return only inactive locations when active=false")
void shouldReturnOnlyInactive() {
var locations = List.of(
activeLocation("Aktiv", StorageType.DRY_STORAGE),
inactiveLocation("Inaktiv", StorageType.COLD_ROOM)
);
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
var result = listStorageLocations.execute(null, false);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Inaktiv");
}
@Test
@DisplayName("should return empty list when no locations match active filter")
void shouldReturnEmptyWhenNoMatch() {
var locations = List.of(
activeLocation("Aktiv", StorageType.DRY_STORAGE)
);
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
var result = listStorageLocations.execute(null, false);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty();
}
}
// ==================== StorageType-Filter ====================
@Nested
@DisplayName("storageType-Filter")
class StorageTypeFilter {
@Test
@DisplayName("should return locations of given storage type")
void shouldReturnByStorageType() {
var coldRooms = List.of(
activeLocation("Kühlraum", StorageType.COLD_ROOM)
);
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", null);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).storageType()).isEqualTo(StorageType.COLD_ROOM);
}
@Test
@DisplayName("should fail with InvalidStorageType for unknown type")
void shouldFailForInvalidStorageType() {
var result = listStorageLocations.execute("INVALID", null);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.InvalidStorageType.class);
verifyNoInteractions(storageLocationRepository);
}
}
// ==================== Kombinierte Filter ====================
@Nested
@DisplayName("kombinierte Filter (storageType + active)")
class CombinedFilter {
@Test
@DisplayName("should return only active locations of given type")
void shouldReturnActiveOfType() {
var coldRooms = List.of(
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM),
inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM)
);
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", true);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Aktiv");
}
@Test
@DisplayName("should return only inactive locations of given type")
void shouldReturnInactiveOfType() {
var coldRooms = List.of(
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM),
inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM)
);
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", false);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Inaktiv");
}
@Test
@DisplayName("should return empty list when no locations match combined filter")
void shouldReturnEmptyWhenNoMatchCombined() {
var coldRooms = List.of(
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM)
);
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", false);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,479 @@
package de.effigenix.application.masterdata;
import de.effigenix.application.masterdata.command.CreateProductCategoryCommand;
import de.effigenix.application.masterdata.command.UpdateProductCategoryCommand;
import de.effigenix.domain.masterdata.*;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
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.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("ProductCategory Use Cases")
class ProductCategoryUseCaseTest {
@Mock private ProductCategoryRepository categoryRepository;
@Mock private ArticleRepository articleRepository;
private ActorId performedBy;
@BeforeEach
void setUp() {
performedBy = ActorId.of("admin-user");
}
// ==================== Helpers ====================
private static ProductCategory existingCategory(String id, String name, String description) {
return ProductCategory.reconstitute(
ProductCategoryId.of(id),
new CategoryName(name),
description
);
}
private static ProductCategory existingCategory(String id, String name) {
return existingCategory(id, name, "Beschreibung für " + name);
}
// ==================== CreateProductCategory ====================
@Nested
@DisplayName("CreateProductCategory")
class CreateProductCategoryTests {
private CreateProductCategory useCase;
@BeforeEach
void setUp() {
useCase = new CreateProductCategory(categoryRepository);
}
@Test
@DisplayName("should create category successfully")
void shouldCreateCategorySuccessfully() {
var cmd = new CreateProductCategoryCommand("Backwaren", "Alle Backwaren");
when(categoryRepository.existsByName(any(CategoryName.class)))
.thenReturn(Result.success(false));
when(categoryRepository.save(any(ProductCategory.class)))
.thenReturn(Result.success(null));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Backwaren");
assertThat(result.unsafeGetValue().description()).isEqualTo("Alle Backwaren");
verify(categoryRepository).existsByName(any(CategoryName.class));
verify(categoryRepository).save(any(ProductCategory.class));
}
@Test
@DisplayName("should create category with null description")
void shouldCreateCategoryWithNullDescription() {
var cmd = new CreateProductCategoryCommand("Backwaren", null);
when(categoryRepository.existsByName(any(CategoryName.class)))
.thenReturn(Result.success(false));
when(categoryRepository.save(any(ProductCategory.class)))
.thenReturn(Result.success(null));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().description()).isNull();
}
@Test
@DisplayName("should fail with ValidationFailure when name is blank")
void shouldFailWhenNameIsBlank() {
var cmd = new CreateProductCategoryCommand(" ", "Beschreibung");
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
verifyNoInteractions(categoryRepository);
}
@Test
@DisplayName("should fail with ValidationFailure when name is null")
void shouldFailWhenNameIsNull() {
var cmd = new CreateProductCategoryCommand(null, "Beschreibung");
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
verifyNoInteractions(categoryRepository);
}
@Test
@DisplayName("should fail with ValidationFailure when name exceeds 100 characters")
void shouldFailWhenNameTooLong() {
var longName = "A".repeat(101);
var cmd = new CreateProductCategoryCommand(longName, "Beschreibung");
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
verifyNoInteractions(categoryRepository);
}
@Test
@DisplayName("should fail with CategoryNameAlreadyExists when name is taken")
void shouldFailWhenNameAlreadyExists() {
var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
when(categoryRepository.existsByName(any(CategoryName.class)))
.thenReturn(Result.success(true));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNameAlreadyExists.class);
verify(categoryRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when existsByName fails")
void shouldFailWhenExistsByNameFails() {
var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
when(categoryRepository.existsByName(any(CategoryName.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
verify(categoryRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
when(categoryRepository.existsByName(any(CategoryName.class)))
.thenReturn(Result.success(false));
when(categoryRepository.save(any(ProductCategory.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
}
}
// ==================== UpdateProductCategory ====================
@Nested
@DisplayName("UpdateProductCategory")
class UpdateProductCategoryTests {
private UpdateProductCategory useCase;
@BeforeEach
void setUp() {
useCase = new UpdateProductCategory(categoryRepository);
}
@Test
@DisplayName("should update name and description successfully")
void shouldUpdateNameAndDescription() {
var categoryId = "cat-1";
var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", "Neue Beschreibung");
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
when(categoryRepository.save(any(ProductCategory.class)))
.thenReturn(Result.success(null));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
assertThat(result.unsafeGetValue().description()).isEqualTo("Neue Beschreibung");
}
@Test
@DisplayName("should update only name when description is null")
void shouldUpdateOnlyName() {
var categoryId = "cat-1";
var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", null);
var original = existingCategory(categoryId, "Alter Name", "Originalbeschreibung");
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
.thenReturn(Result.success(Optional.of(original)));
when(categoryRepository.save(any(ProductCategory.class)))
.thenReturn(Result.success(null));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
assertThat(result.unsafeGetValue().description()).isEqualTo("Originalbeschreibung");
}
@Test
@DisplayName("should update only description when name is null")
void shouldUpdateOnlyDescription() {
var categoryId = "cat-1";
var cmd = new UpdateProductCategoryCommand(categoryId, null, "Neue Beschreibung");
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
when(categoryRepository.save(any(ProductCategory.class)))
.thenReturn(Result.success(null));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Alter Name");
assertThat(result.unsafeGetValue().description()).isEqualTo("Neue Beschreibung");
}
@Test
@DisplayName("should fail with CategoryNotFound when category does not exist")
void shouldFailWhenCategoryNotFound() {
var cmd = new UpdateProductCategoryCommand("nonexistent", "Name", "Beschreibung");
when(categoryRepository.findById(ProductCategoryId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNotFound.class);
verify(categoryRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ValidationFailure when new name is blank")
void shouldFailWhenNewNameIsBlank() {
var categoryId = "cat-1";
var cmd = new UpdateProductCategoryCommand(categoryId, " ", "Beschreibung");
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
verify(categoryRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ValidationFailure when new name exceeds 100 characters")
void shouldFailWhenNewNameTooLong() {
var categoryId = "cat-1";
var longName = "B".repeat(101);
var cmd = new UpdateProductCategoryCommand(categoryId, longName, null);
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
verify(categoryRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var cmd = new UpdateProductCategoryCommand("cat-1", "Name", "Beschreibung");
when(categoryRepository.findById(ProductCategoryId.of("cat-1")))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var categoryId = "cat-1";
var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", null);
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
when(categoryRepository.save(any(ProductCategory.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = useCase.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
}
}
// ==================== ListProductCategories ====================
@Nested
@DisplayName("ListProductCategories")
class ListProductCategoriesTests {
private ListProductCategories useCase;
@BeforeEach
void setUp() {
useCase = new ListProductCategories(categoryRepository);
}
@Test
@DisplayName("should return all categories")
void shouldReturnAllCategories() {
var categories = List.of(
existingCategory("cat-1", "Backwaren"),
existingCategory("cat-2", "Getränke")
);
when(categoryRepository.findAll()).thenReturn(Result.success(categories));
var result = useCase.execute();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2);
}
@Test
@DisplayName("should return empty list when no categories exist")
void shouldReturnEmptyList() {
when(categoryRepository.findAll()).thenReturn(Result.success(List.of()));
var result = useCase.execute();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty();
}
@Test
@DisplayName("should fail with RepositoryFailure when repository fails")
void shouldFailWhenRepositoryFails() {
when(categoryRepository.findAll())
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute();
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
}
}
// ==================== DeleteProductCategory ====================
@Nested
@DisplayName("DeleteProductCategory")
class DeleteProductCategoryTests {
private DeleteProductCategory useCase;
@BeforeEach
void setUp() {
useCase = new DeleteProductCategory(categoryRepository, articleRepository);
}
@Test
@DisplayName("should delete category successfully when not in use")
void shouldDeleteCategorySuccessfully() {
var categoryId = ProductCategoryId.of("cat-1");
when(categoryRepository.findById(categoryId))
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
when(articleRepository.findByCategory(categoryId))
.thenReturn(Result.success(List.of()));
when(categoryRepository.delete(any(ProductCategory.class)))
.thenReturn(Result.success(null));
var result = useCase.execute(categoryId, performedBy);
assertThat(result.isSuccess()).isTrue();
verify(categoryRepository).delete(any(ProductCategory.class));
}
@Test
@DisplayName("should fail with CategoryNotFound when category does not exist")
void shouldFailWhenCategoryNotFound() {
var categoryId = ProductCategoryId.of("nonexistent");
when(categoryRepository.findById(categoryId))
.thenReturn(Result.success(Optional.empty()));
var result = useCase.execute(categoryId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNotFound.class);
verify(categoryRepository, never()).delete(any());
verifyNoInteractions(articleRepository);
}
@Test
@DisplayName("should fail with CategoryInUse when articles reference the category")
void shouldFailWhenCategoryInUse() {
var categoryId = ProductCategoryId.of("cat-1");
when(categoryRepository.findById(categoryId))
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
// Return a non-empty list (using mock since Article is complex)
var mockArticle = mock(Article.class);
when(articleRepository.findByCategory(categoryId))
.thenReturn(Result.success(List.of(mockArticle)));
var result = useCase.execute(categoryId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryInUse.class);
verify(categoryRepository, never()).delete(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var categoryId = ProductCategoryId.of("cat-1");
when(categoryRepository.findById(categoryId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(categoryId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when findByCategory fails")
void shouldFailWhenFindByCategoryFails() {
var categoryId = ProductCategoryId.of("cat-1");
when(categoryRepository.findById(categoryId))
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
when(articleRepository.findByCategory(categoryId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(categoryId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when delete fails")
void shouldFailWhenDeleteFails() {
var categoryId = ProductCategoryId.of("cat-1");
when(categoryRepository.findById(categoryId))
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
when(articleRepository.findByCategory(categoryId))
.thenReturn(Result.success(List.of()));
when(categoryRepository.delete(any(ProductCategory.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = useCase.execute(categoryId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
}
}
}

View file

@ -0,0 +1,909 @@
package de.effigenix.application.masterdata;
import de.effigenix.application.masterdata.command.*;
import de.effigenix.domain.masterdata.*;
import de.effigenix.shared.common.ContactInfo;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
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.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("Supplier Use Cases")
class SupplierUseCaseTest {
@Mock private SupplierRepository supplierRepository;
private final ActorId performedBy = ActorId.of("admin-user");
// ==================== Helpers ====================
private static Supplier existingSupplier(String id) {
return Supplier.reconstitute(
SupplierId.of(id),
new SupplierName("Test Supplier"),
null,
new ContactInfo("+49123456789", null, null),
null,
List.of(),
null,
SupplierStatus.ACTIVE,
OffsetDateTime.now(ZoneOffset.UTC),
OffsetDateTime.now(ZoneOffset.UTC)
);
}
private static Supplier existingSupplierWithCertificate(String id, QualityCertificate certificate) {
return Supplier.reconstitute(
SupplierId.of(id),
new SupplierName("Test Supplier"),
null,
new ContactInfo("+49123456789", null, null),
null,
List.of(certificate),
null,
SupplierStatus.ACTIVE,
OffsetDateTime.now(ZoneOffset.UTC),
OffsetDateTime.now(ZoneOffset.UTC)
);
}
private static Supplier inactiveSupplier(String id) {
return Supplier.reconstitute(
SupplierId.of(id),
new SupplierName("Inactive Supplier"),
null,
new ContactInfo("+49123456789", null, null),
null,
List.of(),
null,
SupplierStatus.INACTIVE,
OffsetDateTime.now(ZoneOffset.UTC),
OffsetDateTime.now(ZoneOffset.UTC)
);
}
private static CreateSupplierCommand validCreateCommand() {
return new CreateSupplierCommand(
"Acme Supplies", "+49123456789", "info@acme.de", "Max Mustermann",
null, null, null, null, null,
null, null
);
}
private static CreateSupplierCommand validCreateCommandWithAddress() {
return new CreateSupplierCommand(
"Acme Supplies", "+49123456789", "info@acme.de", "Max Mustermann",
"Hauptstr.", "1", "12345", "Berlin", "DE",
30, "30 Tage netto"
);
}
// ==================== CreateSupplier ====================
@Nested
@DisplayName("CreateSupplier")
class CreateSupplierTest {
private CreateSupplier createSupplier;
@BeforeEach
void setUp() {
createSupplier = new CreateSupplier(supplierRepository);
}
@Test
@DisplayName("should create supplier with valid minimal data")
void shouldCreateSupplierWithValidData() {
when(supplierRepository.existsByName(any())).thenReturn(Result.success(false));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var result = createSupplier.execute(validCreateCommand(), performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Acme Supplies");
assertThat(result.unsafeGetValue().status()).isEqualTo(SupplierStatus.ACTIVE);
verify(supplierRepository).save(any(Supplier.class));
}
@Test
@DisplayName("should create supplier with full data including address and payment terms")
void shouldCreateSupplierWithFullData() {
when(supplierRepository.existsByName(any())).thenReturn(Result.success(false));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var result = createSupplier.execute(validCreateCommandWithAddress(), performedBy);
assertThat(result.isSuccess()).isTrue();
var supplier = result.unsafeGetValue();
assertThat(supplier.address()).isNotNull();
assertThat(supplier.paymentTerms()).isNotNull();
}
@Test
@DisplayName("should fail when supplier name already exists")
void shouldFailWhenNameAlreadyExists() {
when(supplierRepository.existsByName(any())).thenReturn(Result.success(true));
var result = createSupplier.execute(validCreateCommand(), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNameAlreadyExists.class);
verify(supplierRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ValidationFailure when name is blank")
void shouldFailWhenNameIsBlank() {
var cmd = new CreateSupplierCommand(
"", "+49123456789", null, null,
null, null, null, null, null, null, null
);
var result = createSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
verify(supplierRepository, never()).existsByName(any());
verify(supplierRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ValidationFailure when email format is invalid")
void shouldFailWhenEmailFormatIsInvalid() {
var cmd = new CreateSupplierCommand(
"Acme Supplies", "+49123456789", "invalid-email", null,
null, null, null, null, null, null, null
);
var result = createSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when existsByName fails")
void shouldFailWhenExistsByNameFails() {
when(supplierRepository.existsByName(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = createSupplier.execute(validCreateCommand(), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
when(supplierRepository.existsByName(any())).thenReturn(Result.success(false));
when(supplierRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = createSupplier.execute(validCreateCommand(), performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== UpdateSupplier ====================
@Nested
@DisplayName("UpdateSupplier")
class UpdateSupplierTest {
private UpdateSupplier updateSupplier;
@BeforeEach
void setUp() {
updateSupplier = new UpdateSupplier(supplierRepository);
}
@Test
@DisplayName("should update supplier name")
void shouldUpdateSupplierName() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var cmd = new UpdateSupplierCommand(
supplierId, "New Name", null, null, null,
null, null, null, null, null, null, null
);
var result = updateSupplier.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo("New Name");
}
@Test
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
void shouldFailWhenSupplierNotFound() {
when(supplierRepository.findById(SupplierId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var cmd = new UpdateSupplierCommand(
"nonexistent", "New Name", null, null, null,
null, null, null, null, null, null, null
);
var result = updateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
verify(supplierRepository, never()).save(any());
}
@Test
@DisplayName("should fail with ValidationFailure when new name is blank")
void shouldFailWhenNewNameIsBlank() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
var cmd = new UpdateSupplierCommand(
supplierId, "", null, null, null,
null, null, null, null, null, null, null
);
var result = updateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
verify(supplierRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
when(supplierRepository.findById(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var cmd = new UpdateSupplierCommand(
"supplier-1", "New Name", null, null, null,
null, null, null, null, null, null, null
);
var result = updateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
when(supplierRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var cmd = new UpdateSupplierCommand(
supplierId, "New Name", null, null, null,
null, null, null, null, null, null, null
);
var result = updateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== GetSupplier ====================
@Nested
@DisplayName("GetSupplier")
class GetSupplierTest {
private GetSupplier getSupplier;
@BeforeEach
void setUp() {
getSupplier = new GetSupplier(supplierRepository);
}
@Test
@DisplayName("should return supplier when found")
void shouldReturnSupplierWhenFound() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.of(existingSupplier("supplier-1"))));
var result = getSupplier.execute(supplierId);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().id()).isEqualTo(supplierId);
}
@Test
@DisplayName("should fail with SupplierNotFound when not found")
void shouldFailWhenNotFound() {
var supplierId = SupplierId.of("nonexistent");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.empty()));
var result = getSupplier.execute(supplierId);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when repository fails")
void shouldFailWhenRepositoryFails() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = getSupplier.execute(supplierId);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== ListSuppliers ====================
@Nested
@DisplayName("ListSuppliers")
class ListSuppliersTest {
private ListSuppliers listSuppliers;
@BeforeEach
void setUp() {
listSuppliers = new ListSuppliers(supplierRepository);
}
@Test
@DisplayName("should return all suppliers")
void shouldReturnAllSuppliers() {
var suppliers = List.of(existingSupplier("s-1"), existingSupplier("s-2"));
when(supplierRepository.findAll()).thenReturn(Result.success(suppliers));
var result = listSuppliers.execute();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2);
}
@Test
@DisplayName("should return empty list when no suppliers exist")
void shouldReturnEmptyList() {
when(supplierRepository.findAll()).thenReturn(Result.success(List.of()));
var result = listSuppliers.execute();
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty();
}
@Test
@DisplayName("should fail with RepositoryFailure when findAll fails")
void shouldFailWhenFindAllFails() {
when(supplierRepository.findAll())
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listSuppliers.execute();
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should return suppliers filtered by status")
void shouldReturnSuppliersByStatus() {
var activeSuppliers = List.of(existingSupplier("s-1"));
when(supplierRepository.findByStatus(SupplierStatus.ACTIVE))
.thenReturn(Result.success(activeSuppliers));
var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
}
@Test
@DisplayName("should fail with RepositoryFailure when findByStatus fails")
void shouldFailWhenFindByStatusFails() {
when(supplierRepository.findByStatus(SupplierStatus.ACTIVE))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== ActivateSupplier ====================
@Nested
@DisplayName("ActivateSupplier")
class ActivateSupplierTest {
private ActivateSupplier activateSupplier;
@BeforeEach
void setUp() {
activateSupplier = new ActivateSupplier(supplierRepository);
}
@Test
@DisplayName("should activate an inactive supplier")
void shouldActivateInactiveSupplier() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.of(inactiveSupplier("supplier-1"))));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var result = activateSupplier.execute(supplierId, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(SupplierStatus.ACTIVE);
verify(supplierRepository).save(any(Supplier.class));
}
@Test
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
void shouldFailWhenSupplierNotFound() {
var supplierId = SupplierId.of("nonexistent");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.empty()));
var result = activateSupplier.execute(supplierId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
verify(supplierRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = activateSupplier.execute(supplierId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.of(inactiveSupplier("supplier-1"))));
when(supplierRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = activateSupplier.execute(supplierId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== DeactivateSupplier ====================
@Nested
@DisplayName("DeactivateSupplier")
class DeactivateSupplierTest {
private DeactivateSupplier deactivateSupplier;
@BeforeEach
void setUp() {
deactivateSupplier = new DeactivateSupplier(supplierRepository);
}
@Test
@DisplayName("should deactivate an active supplier")
void shouldDeactivateActiveSupplier() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.of(existingSupplier("supplier-1"))));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var result = deactivateSupplier.execute(supplierId, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().status()).isEqualTo(SupplierStatus.INACTIVE);
verify(supplierRepository).save(any(Supplier.class));
}
@Test
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
void shouldFailWhenSupplierNotFound() {
var supplierId = SupplierId.of("nonexistent");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.empty()));
var result = deactivateSupplier.execute(supplierId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
verify(supplierRepository, never()).save(any());
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = deactivateSupplier.execute(supplierId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var supplierId = SupplierId.of("supplier-1");
when(supplierRepository.findById(supplierId))
.thenReturn(Result.success(Optional.of(existingSupplier("supplier-1"))));
when(supplierRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var result = deactivateSupplier.execute(supplierId, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== RateSupplier ====================
@Nested
@DisplayName("RateSupplier")
class RateSupplierTest {
private RateSupplier rateSupplier;
@BeforeEach
void setUp() {
rateSupplier = new RateSupplier(supplierRepository);
}
@Test
@DisplayName("should rate supplier with valid scores")
void shouldRateSupplierWithValidScores() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var cmd = new RateSupplierCommand(supplierId, 4, 5, 3);
var result = rateSupplier.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
var rating = result.unsafeGetValue().rating();
assertThat(rating).isNotNull();
assertThat(rating.qualityScore()).isEqualTo(4);
assertThat(rating.deliveryScore()).isEqualTo(5);
assertThat(rating.priceScore()).isEqualTo(3);
}
@Test
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
void shouldFailWhenSupplierNotFound() {
when(supplierRepository.findById(SupplierId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var cmd = new RateSupplierCommand("nonexistent", 4, 5, 3);
var result = rateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
}
@Test
@DisplayName("should fail with InvalidRating when score is out of range")
void shouldFailWhenScoreIsOutOfRange() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
var cmd = new RateSupplierCommand(supplierId, 0, 5, 3);
var result = rateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.InvalidRating.class);
}
@Test
@DisplayName("should fail with InvalidRating when score exceeds maximum")
void shouldFailWhenScoreExceedsMax() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
var cmd = new RateSupplierCommand(supplierId, 4, 6, 3);
var result = rateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.InvalidRating.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
when(supplierRepository.findById(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var cmd = new RateSupplierCommand("supplier-1", 4, 5, 3);
var result = rateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
when(supplierRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var cmd = new RateSupplierCommand(supplierId, 4, 5, 3);
var result = rateSupplier.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== AddCertificate ====================
@Nested
@DisplayName("AddCertificate")
class AddCertificateTest {
private AddCertificate addCertificate;
@BeforeEach
void setUp() {
addCertificate = new AddCertificate(supplierRepository);
}
@Test
@DisplayName("should add certificate to supplier")
void shouldAddCertificateToSupplier() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var cmd = new AddCertificateCommand(
supplierId, "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
);
var result = addCertificate.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().certificates()).hasSize(1);
assertThat(result.unsafeGetValue().certificates().getFirst().certificateType()).isEqualTo("ISO 9001");
}
@Test
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
void shouldFailWhenSupplierNotFound() {
when(supplierRepository.findById(SupplierId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var cmd = new AddCertificateCommand(
"nonexistent", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
);
var result = addCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
}
@Test
@DisplayName("should fail with ValidationFailure when certificate type is blank")
void shouldFailWhenCertificateTypeIsBlank() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
var cmd = new AddCertificateCommand(
supplierId, "", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
);
var result = addCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail with ValidationFailure when validUntil is before validFrom")
void shouldFailWhenValidUntilBeforeValidFrom() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
var cmd = new AddCertificateCommand(
supplierId, "ISO 9001", "TUV", LocalDate.of(2027, 1, 1), LocalDate.of(2025, 12, 31)
);
var result = addCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
when(supplierRepository.findById(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var cmd = new AddCertificateCommand(
"supplier-1", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
);
var result = addCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
when(supplierRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var cmd = new AddCertificateCommand(
supplierId, "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
);
var result = addCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
// ==================== RemoveCertificate ====================
@Nested
@DisplayName("RemoveCertificate")
class RemoveCertificateTest {
private RemoveCertificate removeCertificate;
@BeforeEach
void setUp() {
removeCertificate = new RemoveCertificate(supplierRepository);
}
@Test
@DisplayName("should remove existing certificate from supplier")
void shouldRemoveExistingCertificate() {
var supplierId = "supplier-1";
var validFrom = LocalDate.of(2025, 1, 1);
var certificate = new QualityCertificate("ISO 9001", "TUV", validFrom, LocalDate.of(2027, 12, 31));
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplierWithCertificate(supplierId, certificate))));
when(supplierRepository.save(any())).thenReturn(Result.success(null));
var cmd = new RemoveCertificateCommand(supplierId, "ISO 9001", "TUV", validFrom);
var result = removeCertificate.execute(cmd, performedBy);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().certificates()).isEmpty();
}
@Test
@DisplayName("should fail with CertificateNotFound when certificate does not exist")
void shouldFailWhenCertificateNotFound() {
var supplierId = "supplier-1";
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
var cmd = new RemoveCertificateCommand(
supplierId, "ISO 9001", "TUV", LocalDate.of(2025, 1, 1)
);
var result = removeCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.CertificateNotFound.class);
}
@Test
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
void shouldFailWhenSupplierNotFound() {
when(supplierRepository.findById(SupplierId.of("nonexistent")))
.thenReturn(Result.success(Optional.empty()));
var cmd = new RemoveCertificateCommand(
"nonexistent", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1)
);
var result = removeCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when findById fails")
void shouldFailWhenFindByIdFails() {
when(supplierRepository.findById(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var cmd = new RemoveCertificateCommand(
"supplier-1", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1)
);
var result = removeCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
@Test
@DisplayName("should fail with RepositoryFailure when save fails")
void shouldFailWhenSaveFails() {
var supplierId = "supplier-1";
var validFrom = LocalDate.of(2025, 1, 1);
var certificate = new QualityCertificate("ISO 9001", "TUV", validFrom, LocalDate.of(2027, 12, 31));
when(supplierRepository.findById(SupplierId.of(supplierId)))
.thenReturn(Result.success(Optional.of(existingSupplierWithCertificate(supplierId, certificate))));
when(supplierRepository.save(any()))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
var cmd = new RemoveCertificateCommand(supplierId, "ISO 9001", "TUV", validFrom);
var result = removeCertificate.execute(cmd, performedBy);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
}
}
}

View file

@ -0,0 +1,730 @@
package de.effigenix.domain.masterdata;
import de.effigenix.shared.common.Money;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class ArticleTest {
// ==================== Create ====================
@Nested
@DisplayName("create()")
class Create {
@Test
@DisplayName("should create Article with valid data (PIECE_FIXED / FIXED)")
void shouldCreateWithValidData() {
var draft = validDraft();
var result = Article.create(draft);
assertThat(result.isSuccess()).isTrue();
var article = result.unsafeGetValue();
assertThat(article.id()).isNotNull();
assertThat(article.name().value()).isEqualTo("Testwurst");
assertThat(article.articleNumber().value()).isEqualTo("ART-001");
assertThat(article.categoryId()).isNotNull();
assertThat(article.status()).isEqualTo(ArticleStatus.ACTIVE);
assertThat(article.salesUnits()).hasSize(1);
assertThat(article.salesUnits().getFirst().unit()).isEqualTo(Unit.PIECE_FIXED);
assertThat(article.salesUnits().getFirst().priceModel()).isEqualTo(PriceModel.FIXED);
assertThat(article.salesUnits().getFirst().price().amount()).isEqualByComparingTo("9.99");
assertThat(article.supplierReferences()).isEmpty();
assertThat(article.createdAt()).isNotNull();
assertThat(article.updatedAt()).isNotNull();
}
@Test
@DisplayName("should create Article with KG / WEIGHT_BASED")
void shouldCreateWithKgWeightBased() {
var draft = new ArticleDraft("Hackfleisch", "ART-002", UUID.randomUUID().toString(),
Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("12.50"));
var result = Article.create(draft);
assertThat(result.isSuccess()).isTrue();
var su = result.unsafeGetValue().salesUnits().getFirst();
assertThat(su.unit()).isEqualTo(Unit.KG);
assertThat(su.priceModel()).isEqualTo(PriceModel.WEIGHT_BASED);
}
@Test
@DisplayName("should create Article with HUNDRED_GRAM / WEIGHT_BASED")
void shouldCreateWithHundredGramWeightBased() {
var draft = new ArticleDraft("Aufschnitt", "ART-003", UUID.randomUUID().toString(),
Unit.HUNDRED_GRAM, PriceModel.WEIGHT_BASED, new BigDecimal("1.99"));
var result = Article.create(draft);
assertThat(result.isSuccess()).isTrue();
}
@Test
@DisplayName("should create Article with PIECE_VARIABLE / WEIGHT_BASED")
void shouldCreateWithPieceVariableWeightBased() {
var draft = new ArticleDraft("Braten", "ART-004", UUID.randomUUID().toString(),
Unit.PIECE_VARIABLE, PriceModel.WEIGHT_BASED, new BigDecimal("15.00"));
var result = Article.create(draft);
assertThat(result.isSuccess()).isTrue();
}
@Test
@DisplayName("should fail when name is blank")
void shouldFailWhenNameBlank() {
var draft = new ArticleDraft("", "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name is null")
void shouldFailWhenNameNull() {
var draft = new ArticleDraft(null, "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name exceeds 200 characters")
void shouldFailWhenNameTooLong() {
var longName = "A".repeat(201);
var draft = new ArticleDraft(longName, "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should accept name with exactly 200 characters")
void shouldAcceptNameWith200Chars() {
var name = "A".repeat(200);
var draft = new ArticleDraft(name, "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isSuccess()).isTrue();
}
@Test
@DisplayName("should fail when articleNumber is blank")
void shouldFailWhenArticleNumberBlank() {
var draft = new ArticleDraft("Testwurst", "", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when articleNumber is null")
void shouldFailWhenArticleNumberNull() {
var draft = new ArticleDraft("Testwurst", null, UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when articleNumber exceeds 50 characters")
void shouldFailWhenArticleNumberTooLong() {
var longNumber = "A".repeat(51);
var draft = new ArticleDraft("Testwurst", longNumber, UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should accept articleNumber with exactly 50 characters")
void shouldAcceptArticleNumberWith50Chars() {
var number = "A".repeat(50);
var draft = new ArticleDraft("Testwurst", number, UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isSuccess()).isTrue();
}
@Test
@DisplayName("should throw when price is null (Money.tryEuro does not guard null)")
void shouldThrowWhenPriceNull() {
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, null);
org.junit.jupiter.api.Assertions.assertThrows(NullPointerException.class,
() -> Article.create(draft));
}
@Test
@DisplayName("should fail when price is zero")
void shouldFailWhenPriceZero() {
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, BigDecimal.ZERO);
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPrice.class);
}
@Test
@DisplayName("should fail when price is negative")
void shouldFailWhenPriceNegative() {
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("-1.00"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
}
@Test
@DisplayName("should fail when Unit/PriceModel combination is invalid (PIECE_FIXED + WEIGHT_BASED)")
void shouldFailWhenInvalidCombinationPieceFixedWeightBased() {
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, PriceModel.WEIGHT_BASED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
}
@Test
@DisplayName("should fail when Unit/PriceModel combination is invalid (KG + FIXED)")
void shouldFailWhenInvalidCombinationKgFixed() {
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
Unit.KG, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
}
@Test
@DisplayName("should fail when unit is null")
void shouldFailWhenUnitNull() {
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
null, PriceModel.FIXED, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
}
@Test
@DisplayName("should fail when priceModel is null")
void shouldFailWhenPriceModelNull() {
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
Unit.PIECE_FIXED, null, new BigDecimal("9.99"));
var result = Article.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
}
}
// ==================== Update ====================
@Nested
@DisplayName("update()")
class Update {
@Test
@DisplayName("should update name")
void shouldUpdateName() {
var article = createValidArticle();
var updateDraft = new ArticleUpdateDraft("Neuer Name", null);
var result = article.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(article.name().value()).isEqualTo("Neuer Name");
}
@Test
@DisplayName("should update categoryId")
void shouldUpdateCategoryId() {
var article = createValidArticle();
var newCategoryId = UUID.randomUUID().toString();
var updateDraft = new ArticleUpdateDraft(null, newCategoryId);
var result = article.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(article.categoryId().value()).isEqualTo(newCategoryId);
}
@Test
@DisplayName("should update both name and categoryId")
void shouldUpdateBothFields() {
var article = createValidArticle();
var newCategoryId = UUID.randomUUID().toString();
var updateDraft = new ArticleUpdateDraft("Neuer Name", newCategoryId);
var result = article.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(article.name().value()).isEqualTo("Neuer Name");
assertThat(article.categoryId().value()).isEqualTo(newCategoryId);
}
@Test
@DisplayName("should not change fields when all null in draft")
void shouldNotChangeWhenAllNull() {
var article = createValidArticle();
var originalName = article.name().value();
var originalCategoryId = article.categoryId().value();
var updateDraft = new ArticleUpdateDraft(null, null);
var result = article.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(article.name().value()).isEqualTo(originalName);
assertThat(article.categoryId().value()).isEqualTo(originalCategoryId);
}
@Test
@DisplayName("should fail when updated name is blank")
void shouldFailWhenUpdatedNameBlank() {
var article = createValidArticle();
var updateDraft = new ArticleUpdateDraft("", null);
var result = article.update(updateDraft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when updated name exceeds 200 characters")
void shouldFailWhenUpdatedNameTooLong() {
var article = createValidArticle();
var updateDraft = new ArticleUpdateDraft("A".repeat(201), null);
var result = article.update(updateDraft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
}
@Test
@DisplayName("should update updatedAt timestamp")
void shouldUpdateTimestamp() {
var article = createValidArticle();
var originalUpdatedAt = article.updatedAt();
article.update(new ArticleUpdateDraft("Anderer Name", null));
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
}
}
// ==================== Activate / Deactivate ====================
@Nested
@DisplayName("activate() / deactivate()")
class ActivateDeactivate {
@Test
@DisplayName("should deactivate active article")
void shouldDeactivate() {
var article = createValidArticle();
assertThat(article.status()).isEqualTo(ArticleStatus.ACTIVE);
article.deactivate();
assertThat(article.status()).isEqualTo(ArticleStatus.INACTIVE);
}
@Test
@DisplayName("should activate inactive article")
void shouldActivate() {
var article = createValidArticle();
article.deactivate();
article.activate();
assertThat(article.status()).isEqualTo(ArticleStatus.ACTIVE);
}
@Test
@DisplayName("should update updatedAt on deactivate")
void shouldUpdateTimestampOnDeactivate() {
var article = createValidArticle();
var originalUpdatedAt = article.updatedAt();
article.deactivate();
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
}
@Test
@DisplayName("should update updatedAt on activate")
void shouldUpdateTimestampOnActivate() {
var article = createValidArticle();
article.deactivate();
var afterDeactivate = article.updatedAt();
article.activate();
assertThat(article.updatedAt()).isAfterOrEqualTo(afterDeactivate);
}
}
// ==================== Add Sales Unit ====================
@Nested
@DisplayName("addSalesUnit()")
class AddSalesUnit {
@Test
@DisplayName("should add a second SalesUnit with different unit type")
void shouldAddSecondSalesUnit() {
var article = createValidArticle();
var salesUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
.unsafeGetValue();
var result = article.addSalesUnit(salesUnit);
assertThat(result.isSuccess()).isTrue();
assertThat(article.salesUnits()).hasSize(2);
}
@Test
@DisplayName("should fail when adding SalesUnit with duplicate unit type")
void shouldFailWhenDuplicateUnitType() {
var article = createValidArticle();
var duplicate = SalesUnit.create(Unit.PIECE_FIXED, PriceModel.FIXED, Money.euro(new BigDecimal("5.00")))
.unsafeGetValue();
var result = article.addSalesUnit(duplicate);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.DuplicateSalesUnitType.class);
assertThat(article.salesUnits()).hasSize(1);
}
@Test
@DisplayName("should update updatedAt on add")
void shouldUpdateTimestampOnAdd() {
var article = createValidArticle();
var originalUpdatedAt = article.updatedAt();
var salesUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
.unsafeGetValue();
article.addSalesUnit(salesUnit);
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
}
}
// ==================== Remove Sales Unit ====================
@Nested
@DisplayName("removeSalesUnit()")
class RemoveSalesUnit {
@Test
@DisplayName("should remove SalesUnit when more than one exists")
void shouldRemoveWhenMultiple() {
var article = createValidArticle();
var secondUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
.unsafeGetValue();
article.addSalesUnit(secondUnit);
var result = article.removeSalesUnit(secondUnit.id());
assertThat(result.isSuccess()).isTrue();
assertThat(article.salesUnits()).hasSize(1);
}
@Test
@DisplayName("should fail when removing the last SalesUnit")
void shouldFailWhenRemovingLastSalesUnit() {
var article = createValidArticle();
var onlySalesUnitId = article.salesUnits().getFirst().id();
var result = article.removeSalesUnit(onlySalesUnitId);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.MinimumSalesUnitRequired.class);
assertThat(article.salesUnits()).hasSize(1);
}
@Test
@DisplayName("should fail when SalesUnit ID does not exist")
void shouldFailWhenSalesUnitNotFound() {
var article = createValidArticle();
var secondUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
.unsafeGetValue();
article.addSalesUnit(secondUnit);
var nonExistentId = SalesUnitId.generate();
var result = article.removeSalesUnit(nonExistentId);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.SalesUnitNotFound.class);
}
@Test
@DisplayName("should update updatedAt on remove")
void shouldUpdateTimestampOnRemove() {
var article = createValidArticle();
var secondUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
.unsafeGetValue();
article.addSalesUnit(secondUnit);
var afterAdd = article.updatedAt();
article.removeSalesUnit(secondUnit.id());
assertThat(article.updatedAt()).isAfterOrEqualTo(afterAdd);
}
}
// ==================== Update Sales Unit Price ====================
@Nested
@DisplayName("updateSalesUnitPrice()")
class UpdateSalesUnitPrice {
@Test
@DisplayName("should update price of existing SalesUnit")
void shouldUpdatePrice() {
var article = createValidArticle();
var salesUnitId = article.salesUnits().getFirst().id();
var newPrice = Money.euro(new BigDecimal("19.99"));
var result = article.updateSalesUnitPrice(salesUnitId, newPrice);
assertThat(result.isSuccess()).isTrue();
assertThat(article.salesUnits().getFirst().price().amount()).isEqualByComparingTo("19.99");
}
@Test
@DisplayName("should fail when SalesUnit not found")
void shouldFailWhenNotFound() {
var article = createValidArticle();
var nonExistentId = SalesUnitId.generate();
var result = article.updateSalesUnitPrice(nonExistentId, Money.euro(new BigDecimal("5.00")));
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.SalesUnitNotFound.class);
}
@Test
@DisplayName("should fail when new price is zero")
void shouldFailWhenPriceZero() {
var article = createValidArticle();
var salesUnitId = article.salesUnits().getFirst().id();
var result = article.updateSalesUnitPrice(salesUnitId, Money.zero());
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPrice.class);
}
}
// ==================== Supplier References ====================
@Nested
@DisplayName("assignSupplier / removeSupplier")
class SupplierReferences {
@Test
@DisplayName("should add supplier reference")
void shouldAddSupplierReference() {
var article = createValidArticle();
var supplierId = SupplierId.generate();
article.addSupplierReference(supplierId);
assertThat(article.supplierReferences()).containsExactly(supplierId);
}
@Test
@DisplayName("should add multiple supplier references")
void shouldAddMultipleSuppliers() {
var article = createValidArticle();
var supplier1 = SupplierId.generate();
var supplier2 = SupplierId.generate();
article.addSupplierReference(supplier1);
article.addSupplierReference(supplier2);
assertThat(article.supplierReferences()).containsExactlyInAnyOrder(supplier1, supplier2);
}
@Test
@DisplayName("should not duplicate supplier reference")
void shouldNotDuplicateSupplier() {
var article = createValidArticle();
var supplierId = SupplierId.generate();
article.addSupplierReference(supplierId);
article.addSupplierReference(supplierId);
assertThat(article.supplierReferences()).hasSize(1);
}
@Test
@DisplayName("should remove supplier reference")
void shouldRemoveSupplierReference() {
var article = createValidArticle();
var supplierId = SupplierId.generate();
article.addSupplierReference(supplierId);
article.removeSupplierReference(supplierId);
assertThat(article.supplierReferences()).isEmpty();
}
@Test
@DisplayName("should silently ignore removing non-existent supplier")
void shouldIgnoreRemovingNonExistentSupplier() {
var article = createValidArticle();
var supplierId = SupplierId.generate();
article.removeSupplierReference(supplierId);
assertThat(article.supplierReferences()).isEmpty();
}
@Test
@DisplayName("should update updatedAt on add supplier")
void shouldUpdateTimestampOnAddSupplier() {
var article = createValidArticle();
var originalUpdatedAt = article.updatedAt();
article.addSupplierReference(SupplierId.generate());
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
}
@Test
@DisplayName("should update updatedAt on remove supplier")
void shouldUpdateTimestampOnRemoveSupplier() {
var article = createValidArticle();
var supplierId = SupplierId.generate();
article.addSupplierReference(supplierId);
var afterAdd = article.updatedAt();
article.removeSupplierReference(supplierId);
assertThat(article.updatedAt()).isAfterOrEqualTo(afterAdd);
}
}
// ==================== Equality ====================
@Nested
@DisplayName("equals / hashCode")
class Equality {
@Test
@DisplayName("should be equal if same ID")
void shouldBeEqualBySameId() {
var id = ArticleId.generate();
var name = new ArticleName("Testwurst");
var number = new ArticleNumber("ART-001");
var categoryId = ProductCategoryId.generate();
var salesUnit = SalesUnit.reconstitute(
SalesUnitId.generate(), Unit.PIECE_FIXED, PriceModel.FIXED, Money.euro(new BigDecimal("9.99")));
var now = java.time.OffsetDateTime.now();
var article1 = Article.reconstitute(id, name, number, categoryId, List.of(salesUnit),
ArticleStatus.ACTIVE, Set.of(), now, now);
var article2 = Article.reconstitute(id, new ArticleName("Anderer Name"), new ArticleNumber("ART-999"),
ProductCategoryId.generate(), List.of(salesUnit), ArticleStatus.INACTIVE, Set.of(), now, now);
assertThat(article1).isEqualTo(article2);
assertThat(article1.hashCode()).isEqualTo(article2.hashCode());
}
@Test
@DisplayName("should not be equal if different ID")
void shouldNotBeEqualByDifferentId() {
var article1 = createValidArticle();
var article2 = createValidArticle();
assertThat(article1).isNotEqualTo(article2);
}
@Test
@DisplayName("should not be equal to null")
void shouldNotBeEqualToNull() {
var article = createValidArticle();
assertThat(article).isNotEqualTo(null);
}
@Test
@DisplayName("should not be equal to different type")
void shouldNotBeEqualToDifferentType() {
var article = createValidArticle();
assertThat(article).isNotEqualTo("not an article");
}
@Test
@DisplayName("should be equal to itself")
void shouldBeEqualToItself() {
var article = createValidArticle();
assertThat(article).isEqualTo(article);
}
}
// ==================== Helpers ====================
private static ArticleDraft validDraft() {
return new ArticleDraft(
"Testwurst",
"ART-001",
UUID.randomUUID().toString(),
Unit.PIECE_FIXED,
PriceModel.FIXED,
new BigDecimal("9.99")
);
}
private static Article createValidArticle() {
return Article.create(validDraft()).unsafeGetValue();
}
}

View file

@ -0,0 +1,858 @@
package de.effigenix.domain.masterdata;
import de.effigenix.shared.common.Address;
import de.effigenix.shared.common.Money;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class CustomerTest {
// ==================== Create ====================
@Nested
@DisplayName("create()")
class Create {
@Nested
@DisplayName("B2C")
class B2C {
@Test
@DisplayName("should create B2C customer with valid data")
void shouldCreateWithValidData() {
var draft = new CustomerDraft(
"Max Mustermann", CustomerType.B2C,
"+49 123 456", "max@example.com", "Max",
"Hauptstr.", "10", "12345", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isSuccess()).isTrue();
var customer = result.unsafeGetValue();
assertThat(customer.id()).isNotNull();
assertThat(customer.name().value()).isEqualTo("Max Mustermann");
assertThat(customer.type()).isEqualTo(CustomerType.B2C);
assertThat(customer.billingAddress().street()).isEqualTo("Hauptstr.");
assertThat(customer.billingAddress().houseNumber()).isEqualTo("10");
assertThat(customer.billingAddress().postalCode()).isEqualTo("12345");
assertThat(customer.billingAddress().city()).isEqualTo("Berlin");
assertThat(customer.billingAddress().country()).isEqualTo("DE");
assertThat(customer.contactInfo().phone()).isEqualTo("+49 123 456");
assertThat(customer.contactInfo().email()).isEqualTo("max@example.com");
assertThat(customer.contactInfo().contactPerson()).isEqualTo("Max");
assertThat(customer.paymentTerms()).isNull();
assertThat(customer.deliveryAddresses()).isEmpty();
assertThat(customer.frameContract()).isNull();
assertThat(customer.preferences()).isEmpty();
assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
assertThat(customer.createdAt()).isNotNull();
assertThat(customer.updatedAt()).isNotNull();
}
}
@Nested
@DisplayName("B2B")
class B2B {
@Test
@DisplayName("should create B2B customer with valid data")
void shouldCreateWithValidData() {
var draft = new CustomerDraft(
"Firma GmbH", CustomerType.B2B,
"+49 800 123", "info@firma.de", "Herr Schmidt",
"Industriestr.", "5a", "80331", "Muenchen", "DE",
30, "30 Tage netto"
);
var result = Customer.create(draft);
assertThat(result.isSuccess()).isTrue();
var customer = result.unsafeGetValue();
assertThat(customer.name().value()).isEqualTo("Firma GmbH");
assertThat(customer.type()).isEqualTo(CustomerType.B2B);
assertThat(customer.paymentTerms()).isNotNull();
assertThat(customer.paymentTerms().paymentDueDays()).isEqualTo(30);
assertThat(customer.paymentTerms().description()).isEqualTo("30 Tage netto");
}
}
@Nested
@DisplayName("validation failures")
class ValidationFailures {
@Test
@DisplayName("should fail when name is null")
void shouldFailWhenNameNull() {
var draft = new CustomerDraft(
null, CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "12345", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name is blank")
void shouldFailWhenNameBlank() {
var draft = new CustomerDraft(
" ", CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "12345", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name exceeds 200 characters")
void shouldFailWhenNameTooLong() {
var draft = new CustomerDraft(
"A".repeat(201), CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "12345", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should accept name with exactly 200 characters")
void shouldAcceptNameWith200Chars() {
var draft = new CustomerDraft(
"A".repeat(200), CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "12345", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isSuccess()).isTrue();
}
@Test
@DisplayName("should fail when email format is invalid")
void shouldFailWhenEmailInvalid() {
var draft = new CustomerDraft(
"Test", CustomerType.B2C,
"+49 123", "not-an-email", null,
"Str.", null, "12345", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when street is blank")
void shouldFailWhenStreetBlank() {
var draft = new CustomerDraft(
"Test", CustomerType.B2C,
"+49 123", null, null,
"", null, "12345", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when city is blank")
void shouldFailWhenCityBlank() {
var draft = new CustomerDraft(
"Test", CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "12345", "", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when country is not ISO alpha-2")
void shouldFailWhenCountryInvalid() {
var draft = new CustomerDraft(
"Test", CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "12345", "Berlin", "Deutschland",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when postalCode is blank")
void shouldFailWhenPostalCodeBlank() {
var draft = new CustomerDraft(
"Test", CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "", "Berlin", "DE",
null, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when paymentDueDays is negative")
void shouldFailWhenPaymentDueDaysNegative() {
var draft = new CustomerDraft(
"Test", CustomerType.B2C,
"+49 123", null, null,
"Str.", null, "12345", "Berlin", "DE",
-1, null
);
var result = Customer.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
}
}
// ==================== Update ====================
@Nested
@DisplayName("update()")
class Update {
@Test
@DisplayName("should update name")
void shouldUpdateName() {
var customer = createValidCustomer();
var draft = new CustomerUpdateDraft(
"Neuer Name", null, null, null,
null, null, null, null, null,
null, null
);
var result = customer.update(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(customer.name().value()).isEqualTo("Neuer Name");
}
@Test
@DisplayName("should update contactInfo")
void shouldUpdateContactInfo() {
var customer = createValidCustomer();
var draft = new CustomerUpdateDraft(
null, "+49 999 000", "new@example.com", "Frau Mueller",
null, null, null, null, null,
null, null
);
var result = customer.update(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(customer.contactInfo().phone()).isEqualTo("+49 999 000");
assertThat(customer.contactInfo().email()).isEqualTo("new@example.com");
assertThat(customer.contactInfo().contactPerson()).isEqualTo("Frau Mueller");
}
@Test
@DisplayName("should update billingAddress")
void shouldUpdateBillingAddress() {
var customer = createValidCustomer();
var draft = new CustomerUpdateDraft(
null, null, null, null,
"Neue Str.", "20", "54321", "Hamburg", "DE",
null, null
);
var result = customer.update(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(customer.billingAddress().street()).isEqualTo("Neue Str.");
assertThat(customer.billingAddress().city()).isEqualTo("Hamburg");
}
@Test
@DisplayName("should update paymentTerms")
void shouldUpdatePaymentTerms() {
var customer = createValidCustomer();
var draft = new CustomerUpdateDraft(
null, null, null, null,
null, null, null, null, null,
60, "60 Tage netto"
);
var result = customer.update(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(customer.paymentTerms()).isNotNull();
assertThat(customer.paymentTerms().paymentDueDays()).isEqualTo(60);
}
@Test
@DisplayName("should not change fields when all null in draft")
void shouldNotChangeFieldsWhenAllNull() {
var customer = createValidCustomer();
var originalName = customer.name().value();
var originalPhone = customer.contactInfo().phone();
var draft = new CustomerUpdateDraft(
null, null, null, null,
null, null, null, null, null,
null, null
);
var result = customer.update(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(customer.name().value()).isEqualTo(originalName);
assertThat(customer.contactInfo().phone()).isEqualTo(originalPhone);
}
@Test
@DisplayName("should fail update when name is blank")
void shouldFailWhenNameBlank() {
var customer = createValidCustomer();
var draft = new CustomerUpdateDraft(
" ", null, null, null,
null, null, null, null, null,
null, null
);
var result = customer.update(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should fail update when address city is blank")
void shouldFailWhenAddressCityBlank() {
var customer = createValidCustomer();
var draft = new CustomerUpdateDraft(
null, null, null, null,
"Str.", null, "12345", "", "DE",
null, null
);
var result = customer.update(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
}
@Test
@DisplayName("should update updatedAt timestamp")
void shouldUpdateTimestamp() {
var customer = createValidCustomer();
var beforeUpdate = customer.updatedAt();
var draft = new CustomerUpdateDraft(
"Anderer Name", null, null, null,
null, null, null, null, null,
null, null
);
customer.update(draft);
assertThat(customer.updatedAt()).isAfterOrEqualTo(beforeUpdate);
}
}
// ==================== Activate / Deactivate ====================
@Nested
@DisplayName("activate() / deactivate()")
class ActivateDeactivate {
@Test
@DisplayName("should deactivate active customer")
void shouldDeactivate() {
var customer = createValidCustomer();
assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
customer.deactivate();
assertThat(customer.status()).isEqualTo(CustomerStatus.INACTIVE);
}
@Test
@DisplayName("should activate inactive customer")
void shouldActivate() {
var customer = createValidCustomer();
customer.deactivate();
assertThat(customer.status()).isEqualTo(CustomerStatus.INACTIVE);
customer.activate();
assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
}
@Test
@DisplayName("deactivate should update updatedAt")
void deactivateShouldUpdateTimestamp() {
var customer = createValidCustomer();
var before = customer.updatedAt();
customer.deactivate();
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
}
@Test
@DisplayName("activate should update updatedAt")
void activateShouldUpdateTimestamp() {
var customer = createValidCustomer();
customer.deactivate();
var before = customer.updatedAt();
customer.activate();
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
}
}
// ==================== Delivery Addresses ====================
@Nested
@DisplayName("addDeliveryAddress() / removeDeliveryAddress()")
class DeliveryAddresses {
@Test
@DisplayName("should add delivery address")
void shouldAddDeliveryAddress() {
var customer = createValidCustomer();
var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
var deliveryAddress = new DeliveryAddress("Lager Ost", address, "Herr Meier", "Rampe 3");
customer.addDeliveryAddress(deliveryAddress);
assertThat(customer.deliveryAddresses()).hasSize(1);
assertThat(customer.deliveryAddresses().getFirst().label()).isEqualTo("Lager Ost");
assertThat(customer.deliveryAddresses().getFirst().address().city()).isEqualTo("Leipzig");
assertThat(customer.deliveryAddresses().getFirst().contactPerson()).isEqualTo("Herr Meier");
assertThat(customer.deliveryAddresses().getFirst().deliveryNotes()).isEqualTo("Rampe 3");
}
@Test
@DisplayName("should add multiple delivery addresses")
void shouldAddMultipleDeliveryAddresses() {
var customer = createValidCustomer();
var address1 = Address.create("Str. 1", null, "11111", "Stadt A", "DE").unsafeGetValue();
var address2 = Address.create("Str. 2", null, "22222", "Stadt B", "DE").unsafeGetValue();
customer.addDeliveryAddress(new DeliveryAddress("Lager 1", address1, null, null));
customer.addDeliveryAddress(new DeliveryAddress("Lager 2", address2, null, null));
assertThat(customer.deliveryAddresses()).hasSize(2);
}
@Test
@DisplayName("should remove delivery address by label")
void shouldRemoveDeliveryAddressByLabel() {
var customer = createValidCustomer();
var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
customer.addDeliveryAddress(new DeliveryAddress("Lager Ost", address, null, null));
customer.addDeliveryAddress(new DeliveryAddress("Lager West", address, null, null));
customer.removeDeliveryAddress("Lager Ost");
assertThat(customer.deliveryAddresses()).hasSize(1);
assertThat(customer.deliveryAddresses().getFirst().label()).isEqualTo("Lager West");
}
@Test
@DisplayName("should do nothing when removing non-existent label")
void shouldDoNothingWhenRemovingNonExistent() {
var customer = createValidCustomer();
var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
customer.addDeliveryAddress(new DeliveryAddress("Lager Ost", address, null, null));
customer.removeDeliveryAddress("Nicht vorhanden");
assertThat(customer.deliveryAddresses()).hasSize(1);
}
@Test
@DisplayName("addDeliveryAddress should update updatedAt")
void addShouldUpdateTimestamp() {
var customer = createValidCustomer();
var before = customer.updatedAt();
var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
customer.addDeliveryAddress(new DeliveryAddress("L", address, null, null));
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
}
@Test
@DisplayName("removeDeliveryAddress should update updatedAt")
void removeShouldUpdateTimestamp() {
var customer = createValidCustomer();
var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
customer.addDeliveryAddress(new DeliveryAddress("L", address, null, null));
var before = customer.updatedAt();
customer.removeDeliveryAddress("L");
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
}
@Test
@DisplayName("deliveryAddresses should return unmodifiable list")
void shouldReturnUnmodifiableList() {
var customer = createValidCustomer();
var list = customer.deliveryAddresses();
org.junit.jupiter.api.Assertions.assertThrows(
UnsupportedOperationException.class,
() -> list.add(null)
);
}
}
// ==================== Frame Contract ====================
@Nested
@DisplayName("setFrameContract()")
class SetFrameContract {
@Test
@DisplayName("should set frame contract for B2B customer")
void shouldSetFrameContractForB2B() {
var customer = createValidB2BCustomer();
var contract = createValidFrameContract();
var result = customer.setFrameContract(contract);
assertThat(result.isSuccess()).isTrue();
assertThat(customer.frameContract()).isNotNull();
assertThat(customer.frameContract()).isEqualTo(contract);
}
@Test
@DisplayName("should fail setting frame contract for B2C customer")
void shouldFailForB2C() {
var customer = createValidCustomer();
var contract = createValidFrameContract();
var result = customer.setFrameContract(contract);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.FrameContractNotAllowed.class);
assertThat(customer.frameContract()).isNull();
}
@Test
@DisplayName("should remove frame contract")
void shouldRemoveFrameContract() {
var customer = createValidB2BCustomer();
var contract = createValidFrameContract();
customer.setFrameContract(contract);
customer.removeFrameContract();
assertThat(customer.frameContract()).isNull();
}
@Test
@DisplayName("hasActiveFrameContract should return true for active contract")
void shouldReturnTrueForActiveContract() {
var customer = createValidB2BCustomer();
var lineItem = new ContractLineItem(
new ArticleId("ART-001"), Money.euro(new BigDecimal("10.00")),
new BigDecimal("100"), Unit.KG
);
var contract = FrameContract.create(
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
DeliveryRhythm.WEEKLY, List.of(lineItem)
).unsafeGetValue();
customer.setFrameContract(contract);
assertThat(customer.hasActiveFrameContract()).isTrue();
}
@Test
@DisplayName("hasActiveFrameContract should return false when no contract")
void shouldReturnFalseWhenNoContract() {
var customer = createValidB2BCustomer();
assertThat(customer.hasActiveFrameContract()).isFalse();
}
@Test
@DisplayName("getAgreedPrice should return price for existing article")
void shouldReturnAgreedPrice() {
var customer = createValidB2BCustomer();
var articleId = new ArticleId("ART-001");
var price = Money.euro(new BigDecimal("12.50"));
var lineItem = new ContractLineItem(articleId, price, new BigDecimal("100"), Unit.KG);
var contract = FrameContract.create(
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
DeliveryRhythm.WEEKLY, List.of(lineItem)
).unsafeGetValue();
customer.setFrameContract(contract);
var result = customer.getAgreedPrice(articleId);
assertThat(result).isPresent();
assertThat(result.get()).isEqualTo(price);
}
@Test
@DisplayName("getAgreedPrice should return empty for unknown article")
void shouldReturnEmptyForUnknownArticle() {
var customer = createValidB2BCustomer();
var lineItem = new ContractLineItem(
new ArticleId("ART-001"), Money.euro(new BigDecimal("10.00")),
new BigDecimal("100"), Unit.KG
);
var contract = FrameContract.create(
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
DeliveryRhythm.WEEKLY, List.of(lineItem)
).unsafeGetValue();
customer.setFrameContract(contract);
var result = customer.getAgreedPrice(new ArticleId("ART-999"));
assertThat(result).isEmpty();
}
@Test
@DisplayName("getAgreedPrice should return empty when no contract set")
void shouldReturnEmptyWhenNoContract() {
var customer = createValidB2BCustomer();
var result = customer.getAgreedPrice(new ArticleId("ART-001"));
assertThat(result).isEmpty();
}
@Test
@DisplayName("setFrameContract should update updatedAt")
void shouldUpdateTimestamp() {
var customer = createValidB2BCustomer();
var before = customer.updatedAt();
var contract = createValidFrameContract();
customer.setFrameContract(contract);
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
}
}
// ==================== Preferences ====================
@Nested
@DisplayName("setPreferences()")
class Preferences {
@Test
@DisplayName("should set preferences")
void shouldSetPreferences() {
var customer = createValidCustomer();
customer.setPreferences(Set.of(CustomerPreference.BIO, CustomerPreference.REGIONAL));
assertThat(customer.preferences()).containsExactlyInAnyOrder(
CustomerPreference.BIO, CustomerPreference.REGIONAL
);
}
@Test
@DisplayName("should replace existing preferences")
void shouldReplaceExistingPreferences() {
var customer = createValidCustomer();
customer.setPreferences(Set.of(CustomerPreference.BIO, CustomerPreference.HALAL));
customer.setPreferences(Set.of(CustomerPreference.KOSHER));
assertThat(customer.preferences()).containsExactly(CustomerPreference.KOSHER);
}
@Test
@DisplayName("should clear preferences with empty set")
void shouldClearPreferencesWithEmptySet() {
var customer = createValidCustomer();
customer.setPreferences(Set.of(CustomerPreference.BIO));
customer.setPreferences(Set.of());
assertThat(customer.preferences()).isEmpty();
}
@Test
@DisplayName("should add single preference")
void shouldAddSinglePreference() {
var customer = createValidCustomer();
customer.addPreference(CustomerPreference.BIO);
assertThat(customer.preferences()).contains(CustomerPreference.BIO);
}
@Test
@DisplayName("should remove single preference")
void shouldRemoveSinglePreference() {
var customer = createValidCustomer();
customer.addPreference(CustomerPreference.BIO);
customer.addPreference(CustomerPreference.REGIONAL);
customer.removePreference(CustomerPreference.BIO);
assertThat(customer.preferences()).containsExactly(CustomerPreference.REGIONAL);
}
@Test
@DisplayName("setPreferences should update updatedAt")
void shouldUpdateTimestamp() {
var customer = createValidCustomer();
var before = customer.updatedAt();
customer.setPreferences(Set.of(CustomerPreference.TIERWOHL));
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
}
@Test
@DisplayName("preferences should return unmodifiable set")
void shouldReturnUnmodifiableSet() {
var customer = createValidCustomer();
var prefs = customer.preferences();
org.junit.jupiter.api.Assertions.assertThrows(
UnsupportedOperationException.class,
() -> prefs.add(CustomerPreference.BIO)
);
}
}
// ==================== Equality ====================
@Nested
@DisplayName("equals / hashCode")
class Equality {
@Test
@DisplayName("should be equal if same ID")
void shouldBeEqualBySameId() {
var id = CustomerId.generate();
var name = new CustomerName("Test");
var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
var contact = new de.effigenix.shared.common.ContactInfo("+49 1", null, null);
var now = java.time.OffsetDateTime.now();
var c1 = Customer.reconstitute(id, name, CustomerType.B2C, address, contact,
null, List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now);
var c2 = Customer.reconstitute(id, new CustomerName("Anderer Name"), CustomerType.B2B, address, contact,
null, List.of(), null, Set.of(), CustomerStatus.INACTIVE, now, now);
assertThat(c1).isEqualTo(c2);
assertThat(c1.hashCode()).isEqualTo(c2.hashCode());
}
@Test
@DisplayName("should not be equal if different ID")
void shouldNotBeEqualByDifferentId() {
var c1 = createValidCustomer();
var c2 = createValidCustomer();
assertThat(c1).isNotEqualTo(c2);
}
@Test
@DisplayName("should not be equal to null")
void shouldNotBeEqualToNull() {
var customer = createValidCustomer();
assertThat(customer).isNotEqualTo(null);
}
@Test
@DisplayName("should not be equal to different type")
void shouldNotBeEqualToDifferentType() {
var customer = createValidCustomer();
assertThat(customer).isNotEqualTo("not a customer");
}
@Test
@DisplayName("should be equal to itself")
void shouldBeEqualToItself() {
var customer = createValidCustomer();
assertThat(customer).isEqualTo(customer);
}
}
// ==================== Helpers ====================
private Customer createValidCustomer() {
var draft = new CustomerDraft(
"Testkunde B2C", CustomerType.B2C,
"+49 123 456", "test@example.com", null,
"Teststr.", "1", "12345", "Berlin", "DE",
null, null
);
return Customer.create(draft).unsafeGetValue();
}
private Customer createValidB2BCustomer() {
var draft = new CustomerDraft(
"Testkunde B2B GmbH", CustomerType.B2B,
"+49 800 999", "b2b@example.com", "Herr Test",
"Industriestr.", "42", "80331", "Muenchen", "DE",
30, "30 Tage netto"
);
return Customer.create(draft).unsafeGetValue();
}
private FrameContract createValidFrameContract() {
var lineItem = new ContractLineItem(
new ArticleId("ART-001"),
Money.euro(new BigDecimal("10.00")),
new BigDecimal("100"),
Unit.KG
);
return FrameContract.create(
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
DeliveryRhythm.WEEKLY, List.of(lineItem)
).unsafeGetValue();
}
}

View file

@ -0,0 +1,266 @@
package de.effigenix.domain.masterdata;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class ProductCategoryTest {
// ==================== Create ====================
@Nested
@DisplayName("create()")
class Create {
@Test
@DisplayName("should create ProductCategory with valid data")
void shouldCreateWithValidData() {
var draft = new ProductCategoryDraft("Backwaren", "Alle Backwaren-Produkte");
var result = ProductCategory.create(draft);
assertThat(result.isSuccess()).isTrue();
var category = result.unsafeGetValue();
assertThat(category.id()).isNotNull();
assertThat(category.name().value()).isEqualTo("Backwaren");
assertThat(category.description()).isEqualTo("Alle Backwaren-Produkte");
}
@Test
@DisplayName("should create ProductCategory without description")
void shouldCreateWithoutDescription() {
var draft = new ProductCategoryDraft("Backwaren", null);
var result = ProductCategory.create(draft);
assertThat(result.isSuccess()).isTrue();
var category = result.unsafeGetValue();
assertThat(category.name().value()).isEqualTo("Backwaren");
assertThat(category.description()).isNull();
}
@Test
@DisplayName("should fail when name is null")
void shouldFailWhenNameNull() {
var draft = new ProductCategoryDraft(null, "Beschreibung");
var result = ProductCategory.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name is blank")
void shouldFailWhenNameBlank() {
var draft = new ProductCategoryDraft(" ", "Beschreibung");
var result = ProductCategory.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name is empty")
void shouldFailWhenNameEmpty() {
var draft = new ProductCategoryDraft("", "Beschreibung");
var result = ProductCategory.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name exceeds 100 characters")
void shouldFailWhenNameTooLong() {
var longName = "A".repeat(101);
var draft = new ProductCategoryDraft(longName, null);
var result = ProductCategory.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
}
@Test
@DisplayName("should accept name with exactly 100 characters")
void shouldAcceptNameWith100Chars() {
var name = "A".repeat(100);
var draft = new ProductCategoryDraft(name, null);
var result = ProductCategory.create(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().name().value()).isEqualTo(name);
}
@Test
@DisplayName("should generate unique IDs for different categories")
void shouldGenerateUniqueIds() {
var cat1 = ProductCategory.create(new ProductCategoryDraft("Kategorie 1", null)).unsafeGetValue();
var cat2 = ProductCategory.create(new ProductCategoryDraft("Kategorie 2", null)).unsafeGetValue();
assertThat(cat1.id()).isNotEqualTo(cat2.id());
}
}
// ==================== Update ====================
@Nested
@DisplayName("update()")
class Update {
@Test
@DisplayName("should update name")
void shouldUpdateName() {
var category = createValidCategory();
var updateDraft = new ProductCategoryUpdateDraft("Neuer Name", null);
var result = category.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(category.name().value()).isEqualTo("Neuer Name");
}
@Test
@DisplayName("should update description")
void shouldUpdateDescription() {
var category = createValidCategory();
var updateDraft = new ProductCategoryUpdateDraft(null, "Neue Beschreibung");
var result = category.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(category.description()).isEqualTo("Neue Beschreibung");
}
@Test
@DisplayName("should update name and description together")
void shouldUpdateNameAndDescription() {
var category = createValidCategory();
var updateDraft = new ProductCategoryUpdateDraft("Neuer Name", "Neue Beschreibung");
var result = category.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(category.name().value()).isEqualTo("Neuer Name");
assertThat(category.description()).isEqualTo("Neue Beschreibung");
}
@Test
@DisplayName("should not change name when null in draft")
void shouldNotChangeNameWhenNull() {
var category = createValidCategory();
var originalName = category.name().value();
var updateDraft = new ProductCategoryUpdateDraft(null, null);
var result = category.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(category.name().value()).isEqualTo(originalName);
}
@Test
@DisplayName("should not change description when null in draft")
void shouldNotChangeDescriptionWhenNull() {
var category = createValidCategory();
var originalDescription = category.description();
var updateDraft = new ProductCategoryUpdateDraft(null, null);
var result = category.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(category.description()).isEqualTo(originalDescription);
}
@Test
@DisplayName("should fail update when name is blank")
void shouldFailUpdateWhenNameBlank() {
var category = createValidCategory();
var originalName = category.name().value();
var updateDraft = new ProductCategoryUpdateDraft(" ", null);
var result = category.update(updateDraft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
assertThat(category.name().value()).isEqualTo(originalName);
}
@Test
@DisplayName("should fail update when name exceeds 100 characters")
void shouldFailUpdateWhenNameTooLong() {
var category = createValidCategory();
var originalName = category.name().value();
var updateDraft = new ProductCategoryUpdateDraft("A".repeat(101), null);
var result = category.update(updateDraft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
assertThat(category.name().value()).isEqualTo(originalName);
}
}
// ==================== Equality ====================
@Nested
@DisplayName("equals / hashCode")
class Equality {
@Test
@DisplayName("should be equal if same ID")
void shouldBeEqualBySameId() {
var id = ProductCategoryId.generate();
var cat1 = ProductCategory.reconstitute(id, new CategoryName("Backwaren"), "Beschreibung A");
var cat2 = ProductCategory.reconstitute(id, new CategoryName("Getranke"), "Beschreibung B");
assertThat(cat1).isEqualTo(cat2);
assertThat(cat1.hashCode()).isEqualTo(cat2.hashCode());
}
@Test
@DisplayName("should not be equal if different ID")
void shouldNotBeEqualByDifferentId() {
var cat1 = createValidCategory();
var cat2 = createValidCategory();
assertThat(cat1).isNotEqualTo(cat2);
}
@Test
@DisplayName("should be equal to itself")
void shouldBeEqualToItself() {
var category = createValidCategory();
assertThat(category).isEqualTo(category);
}
@Test
@DisplayName("should not be equal to null")
void shouldNotBeEqualToNull() {
var category = createValidCategory();
assertThat(category).isNotEqualTo(null);
}
@Test
@DisplayName("should not be equal to different type")
void shouldNotBeEqualToDifferentType() {
var category = createValidCategory();
assertThat(category).isNotEqualTo("not a category");
}
}
// ==================== Helpers ====================
private ProductCategory createValidCategory() {
var draft = new ProductCategoryDraft("Backwaren", "Alle Backwaren-Produkte");
return ProductCategory.create(draft).unsafeGetValue();
}
}

View file

@ -0,0 +1,653 @@
package de.effigenix.domain.masterdata;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SupplierTest {
// ==================== Create ====================
@Nested
@DisplayName("create()")
class Create {
@Test
@DisplayName("should create Supplier with mandatory fields only")
void shouldCreateWithMandatoryFieldsOnly() {
var draft = new SupplierDraft(
"Lieferant A", "+49 123 456", null, null,
null, null, null, null, null, null, null);
var result = Supplier.create(draft);
assertThat(result.isSuccess()).isTrue();
var supplier = result.unsafeGetValue();
assertThat(supplier.id()).isNotNull();
assertThat(supplier.name().value()).isEqualTo("Lieferant A");
assertThat(supplier.contactInfo().phone()).isEqualTo("+49 123 456");
assertThat(supplier.address()).isNull();
assertThat(supplier.paymentTerms()).isNull();
assertThat(supplier.certificates()).isEmpty();
assertThat(supplier.rating()).isNull();
assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
assertThat(supplier.createdAt()).isNotNull();
assertThat(supplier.updatedAt()).isNotNull();
}
@Test
@DisplayName("should create Supplier with all optional fields")
void shouldCreateWithAllOptionalFields() {
var draft = new SupplierDraft(
"Lieferant B", "+49 999 888", "info@lieferant.de", "Max Mustermann",
"Hauptstr.", "42", "12345", "Berlin", "DE", 30, "Netto 30 Tage");
var result = Supplier.create(draft);
assertThat(result.isSuccess()).isTrue();
var supplier = result.unsafeGetValue();
assertThat(supplier.name().value()).isEqualTo("Lieferant B");
assertThat(supplier.contactInfo().phone()).isEqualTo("+49 999 888");
assertThat(supplier.contactInfo().email()).isEqualTo("info@lieferant.de");
assertThat(supplier.contactInfo().contactPerson()).isEqualTo("Max Mustermann");
assertThat(supplier.address()).isNotNull();
assertThat(supplier.address().street()).isEqualTo("Hauptstr.");
assertThat(supplier.address().city()).isEqualTo("Berlin");
assertThat(supplier.address().country()).isEqualTo("DE");
assertThat(supplier.paymentTerms()).isNotNull();
assertThat(supplier.paymentTerms().paymentDueDays()).isEqualTo(30);
}
@Test
@DisplayName("should fail when name is null")
void shouldFailWhenNameNull() {
var draft = new SupplierDraft(
null, "+49 123 456", null, null,
null, null, null, null, null, null, null);
var result = Supplier.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name is blank")
void shouldFailWhenNameBlank() {
var draft = new SupplierDraft(
" ", "+49 123 456", null, null,
null, null, null, null, null, null, null);
var result = Supplier.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when name exceeds 200 chars")
void shouldFailWhenNameTooLong() {
var longName = "A".repeat(201);
var draft = new SupplierDraft(
longName, "+49 123 456", null, null,
null, null, null, null, null, null, null);
var result = Supplier.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should accept name with exactly 200 chars")
void shouldAcceptNameWith200Chars() {
var name = "A".repeat(200);
var draft = new SupplierDraft(
name, "+49 123 456", null, null,
null, null, null, null, null, null, null);
var result = Supplier.create(draft);
assertThat(result.isSuccess()).isTrue();
}
@Test
@DisplayName("should fail when email format is invalid")
void shouldFailWhenEmailInvalid() {
var draft = new SupplierDraft(
"Lieferant", "+49 123 456", "invalid-email", null,
null, null, null, null, null, null, null);
var result = Supplier.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when address is incomplete (street without city)")
void shouldFailWhenAddressIncomplete() {
var draft = new SupplierDraft(
"Lieferant", "+49 123 456", null, null,
"Hauptstr.", null, null, null, null, null, null);
var result = Supplier.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when country code is invalid")
void shouldFailWhenCountryCodeInvalid() {
var draft = new SupplierDraft(
"Lieferant", "+49 123 456", null, null,
"Hauptstr.", "1", "12345", "Berlin", "Deutschland", null, null);
var result = Supplier.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail when paymentDueDays is negative")
void shouldFailWhenPaymentDueDaysNegative() {
var draft = new SupplierDraft(
"Lieferant", "+49 123 456", null, null,
null, null, null, null, null, -1, null);
var result = Supplier.create(draft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should not create address when street and city are both null")
void shouldNotCreateAddressWhenBothNull() {
var draft = new SupplierDraft(
"Lieferant", "+49 123 456", null, null,
null, null, "12345", null, "DE", null, null);
var result = Supplier.create(draft);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue().address()).isNull();
}
@Test
@DisplayName("should trigger address creation when city is set")
void shouldTriggerAddressCreationWhenCitySet() {
var draft = new SupplierDraft(
"Lieferant", "+49 123 456", null, null,
null, null, null, "Berlin", null, null, null);
var result = Supplier.create(draft);
// Address creation triggered but street is null -> validation failure
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
}
// ==================== Update ====================
@Nested
@DisplayName("update()")
class Update {
@Test
@DisplayName("should update name")
void shouldUpdateName() {
var supplier = createValidSupplier();
var updateDraft = new SupplierUpdateDraft(
"Neuer Name", null, null, null,
null, null, null, null, null, null, null);
var result = supplier.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(supplier.name().value()).isEqualTo("Neuer Name");
}
@Test
@DisplayName("should update contact info")
void shouldUpdateContactInfo() {
var supplier = createValidSupplier();
var updateDraft = new SupplierUpdateDraft(
null, "+49 111 222", "new@mail.de", "Erika Muster",
null, null, null, null, null, null, null);
var result = supplier.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(supplier.contactInfo().phone()).isEqualTo("+49 111 222");
assertThat(supplier.contactInfo().email()).isEqualTo("new@mail.de");
assertThat(supplier.contactInfo().contactPerson()).isEqualTo("Erika Muster");
}
@Test
@DisplayName("should update address")
void shouldUpdateAddress() {
var supplier = createValidSupplier();
var updateDraft = new SupplierUpdateDraft(
null, null, null, null,
"Neue Str.", "10", "54321", "Hamburg", "DE", null, null);
var result = supplier.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(supplier.address()).isNotNull();
assertThat(supplier.address().street()).isEqualTo("Neue Str.");
assertThat(supplier.address().city()).isEqualTo("Hamburg");
}
@Test
@DisplayName("should update payment terms")
void shouldUpdatePaymentTerms() {
var supplier = createValidSupplier();
var updateDraft = new SupplierUpdateDraft(
null, null, null, null,
null, null, null, null, null, 60, "Netto 60 Tage");
var result = supplier.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(supplier.paymentTerms()).isNotNull();
assertThat(supplier.paymentTerms().paymentDueDays()).isEqualTo(60);
}
@Test
@DisplayName("should not change fields when all null in draft")
void shouldNotChangeFieldsWhenAllNull() {
var supplier = createValidSupplier();
var originalName = supplier.name().value();
var originalPhone = supplier.contactInfo().phone();
var updateDraft = new SupplierUpdateDraft(
null, null, null, null,
null, null, null, null, null, null, null);
var result = supplier.update(updateDraft);
assertThat(result.isSuccess()).isTrue();
assertThat(supplier.name().value()).isEqualTo(originalName);
assertThat(supplier.contactInfo().phone()).isEqualTo(originalPhone);
}
@Test
@DisplayName("should fail update when name is blank")
void shouldFailUpdateWhenNameBlank() {
var supplier = createValidSupplier();
var updateDraft = new SupplierUpdateDraft(
"", null, null, null,
null, null, null, null, null, null, null);
var result = supplier.update(updateDraft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should fail update when email is invalid")
void shouldFailUpdateWhenEmailInvalid() {
var supplier = createValidSupplier();
var updateDraft = new SupplierUpdateDraft(
null, "+49 123 456", "not-an-email", null,
null, null, null, null, null, null, null);
var result = supplier.update(updateDraft);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
}
@Test
@DisplayName("should update updatedAt timestamp")
void shouldUpdateTimestamp() {
var supplier = createValidSupplier();
var before = supplier.updatedAt();
supplier.update(new SupplierUpdateDraft(
"Geaendert", null, null, null,
null, null, null, null, null, null, null));
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
}
}
// ==================== Activate / Deactivate ====================
@Nested
@DisplayName("activate() / deactivate()")
class ActivateDeactivate {
@Test
@DisplayName("should deactivate active supplier")
void shouldDeactivate() {
var supplier = createValidSupplier();
assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
supplier.deactivate();
assertThat(supplier.status()).isEqualTo(SupplierStatus.INACTIVE);
}
@Test
@DisplayName("should activate inactive supplier")
void shouldActivate() {
var supplier = createValidSupplier();
supplier.deactivate();
assertThat(supplier.status()).isEqualTo(SupplierStatus.INACTIVE);
supplier.activate();
assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
}
@Test
@DisplayName("should update updatedAt on deactivate")
void shouldUpdateTimestampOnDeactivate() {
var supplier = createValidSupplier();
var before = supplier.updatedAt();
supplier.deactivate();
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
}
@Test
@DisplayName("should update updatedAt on activate")
void shouldUpdateTimestampOnActivate() {
var supplier = createValidSupplier();
supplier.deactivate();
var before = supplier.updatedAt();
supplier.activate();
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
}
}
// ==================== Rate ====================
@Nested
@DisplayName("rate()")
class Rate {
@Test
@DisplayName("should set rating")
void shouldSetRating() {
var supplier = createValidSupplier();
var rating = new SupplierRating(4, 3, 5);
supplier.rate(rating);
assertThat(supplier.rating()).isEqualTo(rating);
assertThat(supplier.rating().qualityScore()).isEqualTo(4);
assertThat(supplier.rating().deliveryScore()).isEqualTo(3);
assertThat(supplier.rating().priceScore()).isEqualTo(5);
}
@Test
@DisplayName("should overwrite existing rating")
void shouldOverwriteExistingRating() {
var supplier = createValidSupplier();
supplier.rate(new SupplierRating(1, 1, 1));
var newRating = new SupplierRating(5, 5, 5);
supplier.rate(newRating);
assertThat(supplier.rating()).isEqualTo(newRating);
}
@Test
@DisplayName("should update updatedAt on rate")
void shouldUpdateTimestampOnRate() {
var supplier = createValidSupplier();
var before = supplier.updatedAt();
supplier.rate(new SupplierRating(3, 3, 3));
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
}
}
// ==================== Certificates ====================
@Nested
@DisplayName("addCertificate() / removeCertificate()")
class Certificates {
@Test
@DisplayName("should add certificate")
void shouldAddCertificate() {
var supplier = createValidSupplier();
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
supplier.addCertificate(cert);
assertThat(supplier.certificates()).hasSize(1);
assertThat(supplier.certificates().getFirst()).isEqualTo(cert);
}
@Test
@DisplayName("should not add duplicate certificate")
void shouldNotAddDuplicate() {
var supplier = createValidSupplier();
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
supplier.addCertificate(cert);
supplier.addCertificate(cert);
assertThat(supplier.certificates()).hasSize(1);
}
@Test
@DisplayName("should add different certificates")
void shouldAddDifferentCertificates() {
var supplier = createValidSupplier();
var cert1 = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
var cert2 = new QualityCertificate("ISO 22000", "Dekra", LocalDate.of(2025, 6, 1), LocalDate.of(2027, 6, 1));
supplier.addCertificate(cert1);
supplier.addCertificate(cert2);
assertThat(supplier.certificates()).hasSize(2);
}
@Test
@DisplayName("should remove certificate")
void shouldRemoveCertificate() {
var supplier = createValidSupplier();
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
supplier.addCertificate(cert);
supplier.removeCertificate(cert);
assertThat(supplier.certificates()).isEmpty();
}
@Test
@DisplayName("should not fail when removing non-existent certificate")
void shouldNotFailWhenRemovingNonExistent() {
var supplier = createValidSupplier();
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
supplier.removeCertificate(cert);
assertThat(supplier.certificates()).isEmpty();
}
@Test
@DisplayName("should return unmodifiable certificate list")
void shouldReturnUnmodifiableCertificateList() {
var supplier = createValidSupplier();
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
supplier.addCertificate(cert);
var certs = supplier.certificates();
org.junit.jupiter.api.Assertions.assertThrows(
UnsupportedOperationException.class,
() -> certs.add(new QualityCertificate("HACCP", "Bureau Veritas", null, null))
);
}
@Test
@DisplayName("should update updatedAt on addCertificate")
void shouldUpdateTimestampOnAdd() {
var supplier = createValidSupplier();
var before = supplier.updatedAt();
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
supplier.addCertificate(cert);
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
}
@Test
@DisplayName("should update updatedAt on removeCertificate")
void shouldUpdateTimestampOnRemove() {
var supplier = createValidSupplier();
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
supplier.addCertificate(cert);
var before = supplier.updatedAt();
supplier.removeCertificate(cert);
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
}
}
// ==================== Expired / Expiring Certificates ====================
@Nested
@DisplayName("expiredCertificates() / certificatesExpiringSoon()")
class CertificateQueries {
@Test
@DisplayName("should return expired certificates")
void shouldReturnExpiredCertificates() {
var supplier = createValidSupplier();
var expired = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2020, 1, 1), LocalDate.of(2022, 1, 1));
var valid = new QualityCertificate("HACCP", "Dekra", LocalDate.of(2025, 1, 1), LocalDate.of(2030, 1, 1));
supplier.addCertificate(expired);
supplier.addCertificate(valid);
var result = supplier.expiredCertificates();
assertThat(result).hasSize(1);
assertThat(result.getFirst().certificateType()).isEqualTo("ISO 9001");
}
@Test
@DisplayName("should return empty list when no certificates are expired")
void shouldReturnEmptyWhenNoneExpired() {
var supplier = createValidSupplier();
var valid = new QualityCertificate("HACCP", "Dekra", LocalDate.of(2025, 1, 1), LocalDate.of(2030, 1, 1));
supplier.addCertificate(valid);
assertThat(supplier.expiredCertificates()).isEmpty();
}
@Test
@DisplayName("should return certificates expiring within given days")
void shouldReturnCertificatesExpiringSoon() {
var supplier = createValidSupplier();
var expiringSoon = new QualityCertificate("ISO 9001", "TUV",
LocalDate.of(2025, 1, 1), LocalDate.now().plusDays(15));
var farFuture = new QualityCertificate("HACCP", "Dekra",
LocalDate.of(2025, 1, 1), LocalDate.now().plusDays(365));
supplier.addCertificate(expiringSoon);
supplier.addCertificate(farFuture);
var result = supplier.certificatesExpiringSoon(30);
assertThat(result).hasSize(1);
assertThat(result.getFirst().certificateType()).isEqualTo("ISO 9001");
}
@Test
@DisplayName("should not include certificates without validUntil in expiring soon")
void shouldNotIncludeCertsWithoutValidUntil() {
var supplier = createValidSupplier();
var noExpiry = new QualityCertificate("Bio", "Bioland", null, null);
supplier.addCertificate(noExpiry);
assertThat(supplier.certificatesExpiringSoon(30)).isEmpty();
}
}
// ==================== Equality ====================
@Nested
@DisplayName("equals / hashCode")
class Equality {
@Test
@DisplayName("should be equal if same ID")
void shouldBeEqualBySameId() {
var id = SupplierId.generate();
var now = OffsetDateTime.now(ZoneOffset.UTC);
var name = new SupplierName("Lieferant");
var contactInfo = new de.effigenix.shared.common.ContactInfo("+49 123", null, null);
var s1 = Supplier.reconstitute(id, name, null, contactInfo, null, List.of(), null, SupplierStatus.ACTIVE, now, now);
var s2 = Supplier.reconstitute(id, new SupplierName("Anderer Name"), null, contactInfo, null, List.of(), null, SupplierStatus.INACTIVE, now, now);
assertThat(s1).isEqualTo(s2);
assertThat(s1.hashCode()).isEqualTo(s2.hashCode());
}
@Test
@DisplayName("should not be equal if different ID")
void shouldNotBeEqualByDifferentId() {
var s1 = createValidSupplier();
var s2 = createValidSupplier();
assertThat(s1).isNotEqualTo(s2);
}
@Test
@DisplayName("should be equal to itself")
void shouldBeEqualToItself() {
var supplier = createValidSupplier();
assertThat(supplier).isEqualTo(supplier);
}
@Test
@DisplayName("should not be equal to null")
void shouldNotBeEqualToNull() {
var supplier = createValidSupplier();
assertThat(supplier).isNotEqualTo(null);
}
@Test
@DisplayName("should not be equal to different type")
void shouldNotBeEqualToDifferentType() {
var supplier = createValidSupplier();
assertThat(supplier).isNotEqualTo("not a supplier");
}
}
// ==================== Helpers ====================
private Supplier createValidSupplier() {
var draft = new SupplierDraft(
"Test Lieferant", "+49 123 456", "test@example.com", "Max Muster",
null, null, null, null, null, null, null);
return Supplier.create(draft).unsafeGetValue();
}
}